现代c++编程c++11/14/17/20:Chapter 03: Language Runtime Enhancements

3.1 Lambda Expression

Lambda表达式是现代c++中最重要的特性之一,而Lambda表达式实际上提供了一个类似于匿名函数的特性。匿名函数在需要函数时使用,但您不希望使用名称调用函数。实际上有很多很多这样的场景。所以匿名函数几乎是现代编程语言的标准。

3.1.1 Basic

Lambda表达式的基本语法如下:

[capture list] (parameter list) mutable(optional) exception attribute -> return type {
// function body
}

除了[capture list]中的内容外,以上语法规则都很好理解,只是通用函数的函数名被省略了。返回值的形式是->(我们在前一节的尾部返回类型中已经提到过这一点)。

所谓的捕获列表可以理解为一种参数类型。默认情况下,lambda表达式的内部函数体不能使用函数体之外的变量。在这时,捕获列表可以用于传输外部数据。根据所通过的行为,捕获列表还分为以下几种类型:

  1. Value capture 与参数传递类似,值捕获基于变量可以被复制的事实,除了捕获的变量是在创建lambda表达式时复制的,而不是在调用时复制:
void lambda_value_capture() {
	int value = 1;
	auto copy_value = [value] {
	return value;
	};
	value = 100;
	auto stored_value = copy_value();
	std::cout << "stored_value = " << stored_value << std::endl;
	// At this moment, stored_value == 1, and value == 100.
	// Because copy_value has copied when its was created.
}
  1. Reference capture 与引用传递类似,引用捕获保存引用和值的更改。
void lambda_reference_capture() {
	int value = 1;
	auto copy_value = [&value] {
	return value;
	};
	value = 100;
	auto stored_value = copy_value();
	std::cout << "stored_value = " << stored_value << std::endl;
	// At this moment, stored_value == 100, value == 100.
	// Because copy_value stores reference
}
  1. Implicit capture 手动编写捕获列表有时非常复杂。这种机械工作可以由编译器处理。此时,您可以向编译器编写&或=来声明引用或值捕获。总而言之,capture为lambda表达式提供了使用外部值的能力。四种最常见的捕获列表形式是:

    • [] 空的捕获列表
    • [name1, name2,…]捕获一系列变量
    • [&]引用捕获,让编译器自己派生捕获列表
    • 值捕获,让编译器执行派生应用程序列表
  2. Expression capture
    本节需要理解右值引用和智能指针,这些将在后面提到。
    上面提到的值捕获和引用捕获是在外部范围中声明的变量,因此这些捕获方法捕获左值而不捕获右值。
    c++ 14为我们提供了方便,允许用任意表达式初始化捕获的成员,这允许捕获右值。根据表达式判断被声明的捕获变量的类型,该判断与使用auto相同。

void lambda_expression_capture() {
	auto important = std::make_unique<int>(1);
	auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
		return x+y+v1+(*v2);
	};
	std::cout << add(3,4) << std::endl;
}

在上面的代码中,important是一个无法捕获的独占指针。此时,我们需要将其转换为右值并在表达式中对其进行初始化。

3.1.2 Generic Lambda

在上一节中,我们提到auto关键字不能在参数列表中使用,因为它会与模板的功能冲突。但是Lambda表达式不是普通的函数,所以Lambda表达式没有模板化。这给我们带来了一些麻烦:参数表不能一般化,必须明确参数表的类型。

幸运的是,这个问题只存在于c++ 11中,从c++ 14开始。Lambda函数的形式参数可以使用auto关键字生成通用意义:

void lambda_generic() {
	auto generic = [](auto x, auto y) {
		return x+y;
	};
	std::cout << generic(1, 2) << std::endl;
	std::cout << generic(1.1, 2.2) << std::endl;
}

3.2 Function Object Wrapper

尽管这些特性是标准库的一部分,并且在运行时中找不到,但它增强了c++语言的运行时功能。这部分内容也很重要,所以放在这里作为介绍。

3.2.1 std::function

Lambda表达式的本质是类类型(称为闭包类型)的对象,它类似于函数对象类型(称为闭包对象)。当Lambda表达式的捕获列表为空时,闭包对象也可以转换为函数指针值以进行传递。例如:

#include <iostream>
using foo = void(int); // function pointer
void functional(foo f) {
	f(1);
}
int main() {
	auto f = [](int value) {
		std::cout << value << std::endl;
	};
	functional(f); // call by function pointer
	f(1); // call by lambda expression
	return 0;
}

上面的代码给出了两种不同的调用形式,一种是作为函数类型调用Lambda,另一种是直接调用Lambda表达式。在c++ 11中,这些概念是统一的。可以调用的对象类型统称为可调用类型。这个类型是由std::function引入的。

function是一个通用的多态函数包装器,它的实例可以存储、复制和调用任何可以调用的目标实体。它本身也是一个现有的可调用的对象。它是一个类型安全的实体包(相对而言,对函数指针的调用不是类型安全的),换句话说,一个函数的容器。当我们有一个函数容器时,我们可以更容易地将函数和函数指针作为对象处理。

#include <functional>
#include <iostream>

int foo(int para) {
	return para;
}

int main() {
// std::function wraps a function that take int paremeter and returns int value
	std::function<int(int)> func = foo;
	
	int important = 10;

	std::function<int(int)> func2 = [&](int value) -> int {
		return 1+value+important;
	};
	
	std::cout << func(10) << std::endl;
	std::cout << func2(10) << std::endl;
}

3.2.2 std::bind and std::placeholder

和std::bind用于绑定函数调用的参数。它解决了我们不可能每次都能得到一个函数的所有参数的要求。通过这个函数,我们可以将部分调用参数预先绑定到函数,成为一个新的对象,然后在参数完成后完成调用。

int foo(int a, int b, int c) {
;
}
int main() {
// bind parameter 1, 2 on function foo, and use std::placeholders::_1 as placeholder
// for the first parameter.
auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
// when call bindFoo, we only need one param left
bindFoo(1);
}

提示:注意auto关键字的魔力。有时我们可能不熟悉函数的返回类型,但我们可以通过使用auto来绕过这个问题。

3.3 rvalue reference

右值引用与Lambda表达式一起是c++ 11引入的重要特性之一。它的引入解决了c++中大量的历史问题。消除多余的开销,如std::vector, std::string,使函数对象容器std::函数成为可能。

3.3.1 lvalue, rvalue, prvalue, xvalue

要理解右值引用是关于什么的,您必须清楚地理解左值和右值。
顾名思义,左值是赋值符号左边的值。确切地说,左值是在表达式(不一定是赋值表达式)之后仍然存在的持久对象。
右值,右值,右边的值指的是表达式结束后不再存在的临时对象。在c++ 11中,为了引入强大的右值引用,右值的概念被进一步划分为:prvalue和xvalue。

pvalue,纯右值,纯右值,或者纯文字,比如10,true;求值的结果要么相当于一个文字或匿名临时对象(例如1+2)、非引用返回的临时变量、操作表达式生成的临时变量、原始文本和Lambda表达式都是纯右值。

注意,一个字符串在类中变成了右值,在其他情况下仍然是左值(例如,在函数中):

class Foo {
	const char*&& right = "this is a rvalue";
public:
	void bar() {
		right = "still rvalue"; // the string literal is a rvalue
	}
};
int main() {
	const char* const &left = "this is an lvalue"; // the string literal is an lvalue
}

xvalue, expiring value,过期值是c++ 11提出的引入右值引用的概念(所以在传统c++中,纯右值和右值是同一个概念),一个被破坏但可以移动的值。

这将是有点难以理解的xvalue,让我们看看这样的代码:

std::vector<int> foo() {
	std::vector<int> temp = {1, 2, 3, 4};
	return temp;
}
std::vector<int> v = foo();

在这样的代码中,就传统的理解而言,函数foo的返回值temp被内部创建,然后赋值给v,而当v得到这个对象时,整个temp被复制。然后销毁temp,如果这个temp非常大,这将导致很多额外的开销(这是传统c++一直被批评的问题)。在最后一行中,v是左值,foo()返回的值是右值(也是纯右值)。但是,v可以被其他变量捕获,foo()生成的返回值被用作临时值。一旦被v复制,它将立即被销毁,无法获得或修改。xvalue定义了一种行为,在这种行为中,可以在移动临时值的同时识别临时值。

在c++ 11之后,编译器为我们做了一些工作,其中lvalue temp被进行了隐式的右值转换,相当于static_cast<std::vector &&>(temp),这里v将foo返回的值移动到本地。这就是我们稍后将提到的move语义。

3.3.2 rvalue reference and lvalue reference

要获得一个xvalue,您需要使用右值引用的声明:T &&,其中T是类型。rvalue引用的语句扩展了这个临时值的生命周期,只要变量是活动的,xvalue就会继续存在。

c++ 11提供了std::move方法来无条件地将左值参数转换为右值。有了它,我们可以很容易地获得一个rvalue临时对象,例如:

#include <iostream>
#include <string>

void reference(std::string& str) {
	std::cout << "lvalue" << std::endl;
}

void reference(std::string&& str) {
	std::cout << "rvalue" << std::endl;
}

int main()
{
	std::string lv1 = "string,"; // lv1 is a lvalue
	// std::string&& r1 = s1; // illegal, rvalue can't ref to lvalue
	std::string&& rv1 = std::move(lv1); // legal, std::move can convert lvalue to rvalue
	std::cout << rv1 << std::endl; // string,
	const std::string& lv2 = lv1 + lv1; // legal, const lvalue reference can extend temp variable's lifecycle
	// lv2 += "Test"; // illegal, const ref can't be modified
	std::cout << lv2 << std::endl; // string,string
	std::string&& rv2 = lv1 + lv2; // legal, rvalue ref extend lifecycle
	rv2 += "string"; // legal, non-const reference can be modified
	std::cout << rv2 << std::endl; // string,string,string,string
	reference(rv2); // output: lvalue
	
	return 0;
}

rv2指的是一个右值,但因为它是一个引用,所以rv2仍然是一个左值。

注意这里有一个非常有趣的历史问题,让我们看看下面的代码:

#include <iostream>
int main() {
	// int &a = std::move(1); // illegal, non-const lvalue reference cannot ref rvalue
	const int &b = std::move(1); // legal, const lvalue reference can
	std::cout << b << std::endl;
}

第一个问题,为什么不允许非线性引用绑定到非左值?这是因为这种方法存在逻辑错误:

void increase(int & v) {
	v++;
}
void foo() {
	double s = 1;
	increase(s);
}

因为int&不能引用double类型的参数,所以必须生成一个临时值来保存s的值。因此,当increase()修改这个临时值时,在调用完成后,s本身不会被修改。

第二个问题,为什么常量引用允许绑定到非左值?原因很简单,因为Fortran需要它。

3.3.3 Move semantics

传统c++通过复制构造函数和赋值操作符为类对象设计了复制/复制的概念,但是为了实现资源的移动,调用者必须先使用复制然后析构的方法,否则你需要自己实现移动对象的接口。想象一下,把你的家直接搬到你的新家,而不是复制(重新购买)到你的新家。扔掉(毁坏)所有原始的东西是非常反人类的行为。
传统c++没有区分“移动”和“复制”的概念,导致大量的数据复制,浪费时间和空间。右值引用的出现解决了这两个概念的混淆,例如:

#include <iostream>
class A {
public:
	int *pointer;
	A():pointer(new int(1)) {
		std::cout << "construct" << pointer << std::endl;
	}
	A(A& a):pointer(new int(*a.pointer)) {
		std::cout << "copy" << pointer << std::endl;
	} // meaningless object copy
	
	A (A&& a):pointer(a.pointer) {
		a.pointer = nullptr;
		std::cout << "move" << pointer << std::endl;
	}
	~A(){
		std::cout << "destruct" << pointer << std::endl;
		delete pointer;
	}
};

// avoid compiler optimization
A return_rvalue(bool test) {
	A a,b;
	if(test) return a; // equal to static_cast<A&&>(a);
	else return b; // equal to static_cast<A&&>(b);
}

int main() {
	A obj = return_rvalue(false);
	std::cout << "obj:" << std::endl;
	std::cout << obj.pointer << std::endl;
	std::cout << *obj.pointer << std::endl;
	
	return 0;
}

construct0x31498
construct0x314c8
move0x314c8
destruct0
destruct0x31498
obj:
0x314c8
1
destruct0x314c8

在上述代码中:

  • 首先在return_rvalue内部构造两个A对象,并获取两个构造函数的输出;
  • 在函数返回后,它将生成一个xvalue,该xvalue被A的移动结构引用A(A&&),从而扩展生命周期,并在rvalue中获取指针并保存到obj。在中间,指向xvalue的指针被设置为nullptr,这可以防止内存区域被破坏。

这样可以避免无意义的复制构造并提高性能。让我们来看一个涉及到标准库的例子:

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string
int main() {
	std::string str = "Hello world.";
	std::vector<std::string> v;
	// use push_back(const T&), copy
	v.push_back(str);
	// "str: Hello world."
	std::cout << "str: " << str << std::endl;
	// use push_back(const T&&), no copy
	// the string will be moved to vector, and therefore std::move can reduce copy cost
	v.push_back(std::move(str));
	// str is empty now
	std::cout << "str: " << str << std::endl;
	return 0;
}

3.3.4 Perfect forwarding

如前所述,声明的右值引用实际上是左值。这给我们带来了参数化(传递)的问题:

#include <iostream>
#include <utility>
void reference(int& v) {
    std::cout << "lvalue reference" << std::endl;
}
void reference(int&& v) {
    std::cout << "rvalue reference" << std::endl;
}
template <typename T>
void pass(T&& v) {
    std::cout << " normal param passing: ";
    reference(v);
}

int main() {
    std::cout << "rvalue pass:" << std::endl;
    pass(1);
    std::cout << "lvalue pass:" << std::endl;
    int l = 1;
    pass(l);
    
    return 0;
}

rvalue pass:
 normal param passing: lvalue reference
lvalue pass:
 normal param passing: lvalue reference

对于pass(1),虽然值是右值,但由于v是一个引用,所以它也是一个左值。因此,reference(v)将调用reference(int&)并输出左值。对于pass(l), l是一个左值,为什么它被成功传递给pass(T&&)?
这是基于引用崩溃规则:在传统的c++中,我们不能继续引用一个引用类型。然而,随着右值引用的出现,c++放宽了这种做法,产生了一个引用折叠规则,允许我们引用引用,左值和右值。但请遵循以下规则:

Function parameterArgument parameter typePost-derivation function parameter type
T&lvalue refT&
T&rvalue refT&
T&&lvalue refT&
T&&rvalue refT&&

因此,在模板函数中使用T&&可能无法进行右值引用,当传递左值时,对该函数的引用将派生为左值。更准确地说,无论模板参数是什么类型的引用,当且仅当参数类型是正确的引用时,模板参数都可以被派生为正确的引用类型。这使得v成功地交付了lvalue。

完美转发就是基于上述规则。所谓完美转发就是让我们传递参数,保留原始参数类型(左值引用保留左值引用,右值引用保留右值引用)。为了解决这个问题,我们应该使用std::forward来转发(传递)参数:

#include <iostream>
#include <utility>

void reference(int& v) {
    std::cout << "lvalue reference" << std::endl;
}
void reference(int&& v) {
    std::cout << "rvalue reference" << std::endl;
}

template <typename T>
void pass(T&& v) {
    std::cout << " normal param passing: ";
    reference(v);
    std::cout << " std::move param passing: ";
    reference(std::move(v));
    std::cout << " std::forward param passing: ";
    reference(std::forward<T>(v));
    std::cout << "static_cast<T&&> param passing: ";
    reference(static_cast<T&&>(v));
}
int main() {
    std::cout << "rvalue pass:" << std::endl;
    pass(1);
    std::cout << "lvalue pass:" << std::endl;
    int l = 1;
    pass(l);
    return 0;
}
rvalue pass:
 normal param passing: lvalue reference
 std::move param passing: rvalue reference
 std::forward param passing: rvalue reference
static_cast<T&&> param passing: rvalue reference
lvalue pass:
 normal param passing: lvalue reference
 std::move param passing: rvalue reference
 std::forward param passing: lvalue reference
static_cast<T&&> param passing: lvalue reference

无论传递参数是左值还是右值,普通的传递参数都将以左值的形式转发参数。所以std::move总是接受一个左值,它将调用转发到reference(int&&)以输出右值引用。

只有std::forward不会导致任何额外拷贝,并且完美地将(传递)函数的参数传递给其他内部调用的函数。

std::forward和std::move一样,什么都不做。move只是将左值转换为右值。std::转发只是一个简单的参数转换。从现象上看,std::forward(v)与static_cast<T&&>(v)是完全相同的。

读者可能会好奇为什么语句可以返回两种类型的返回值。让我们快速看一下std::forward的具体实现。std::forward包含两个重载:

template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
	static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
	" substituting _Tp is an lvalue reference type");
	return static_cast<_Tp&&>(__t);
}

在这个实现中,std::remove_reference的函数是消除类型中的引用。而std::is_lvalue_reference用于检查类型派生是否正确,在std::forward的第二个实现中检查接收到的值是否确实是左值,这反过来反映了折叠规则。

当std::forward接受左值时,_Tp被泛化派生为左值,因此返回值为左值; 当它接受右值时,_Tp被派生为一个右值引用,并且基于折叠规则,返回值成为&& + &&的右值。可以看出std::forward的原则是巧妙利用模板类型派生中的差异。

至此,我们可以回答这个问题:为什么auto&&是使用循环语句最安全的方法?因为当auto被推到不同的左值和右值引用时,带有&&的折叠组合被完美转发。

3.4 Conclusion

这一章介绍了现代c++中最重要的运行时增强,我相信这一节中提到的所有特性都是值得了解的:
Lambda表达式 函数对象容器std::function 右值引用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
现代C++程序设计(原书第2版)》图文并茂,通俗易懂,真正做到寓教于乐,是一本难得的C++面向对象设计入门教材。 出版者的话 译者序 前言 第1章 C++概述与软件开发 1.1 什么是C语言和C++ 1.1.1 C和C++历史回顾 1.1.2 C/C++是一门编译语言 1.1.3 为什么许多程序员都选择C++ 1.2 什么是面向对象 1.2.1 C++程序并不一定是面向对象的 1.2.2 一个简单的面向对象程序示例 1.2.3 面向对象的软件更有优势 1.3 结构化设计与面向对象设计 1.3.1 ATM——结构化设计 1.3.2 采用面向对象方法的ATM——究竟是谁的任务 1.3.3 汽车维护——结构化设计 1.3.4 采用面向对象方法的汽车维护——究竟是谁的任务 1.4 软件开发技术概述 1.5 问题发现与解决 复习题 第2章 C++的入门知识 2.1 编程基础 2.1.1 算法设计 2.1.2 正确的软件开发步骤 2.2 专业术语及工程创建 2.3 C++程序的一般格式 2.3.1 “Hello World!”程序 2.3.2 “How’s the Weather?”程序 2.4 程序的数据及数据类型 2.4.1 C++的数据类型 2.4.2 容器=数据类型,标签=变量名 2.4.3 数据类型修饰符 2.4.4 问题分析:整型数据究竟有多大 2.5 C++中的变量声明 2.5.1 C++的命名规则 2.5.2 在哪里声明变量 2.6 C++中的运算符 2.6.1 计算路程的程序 2.6.2 从键盘输入程序所需数据 2.6.3 赋值运算符 2.6.4 运算符的优先级 2.6.5 数据类型及其存储的值 2.6.6 算术运算符 2.6.7 自增运算符和自减运算符 2.6.8 复合赋值运算符 2.7 #define、const和数据类型转换 2.7.1 #define预处理指令 2.7.2 const修饰符 2.7.3 const比#define好吗 2.7.4 数据类型转换 2.8 关于键盘输入和屏幕输出的更多内容 2.8.1 转义序列 2.8.2 ios格式标记 2.8.3 流的IO控制符 2.9 开始使用类和对象、C++string类 2.10 练习 复习题 第3章 控制语句和循环 3.1 关系运算符和逻辑运算符 3.2 if语句 3.2.1 if-else语句 3.2.2 问题分析:在if语句中使用大括号 3.2.3 if-else if-else语句 3.2.4 低效的编程方法 3.2.5 if-else程序示例 3.2.6 嵌套if-else语句 3.2.7 条件运算符“?” 3.3 switch语句 3.4 循环 3.4.1 括号的用法 3.4.2 无限循环 3.5 for循环 3.5.1 不要改变循环索引 3.5.2 for循环示例 3.6 while循环 3.7 do while循环 3.8 跳转语句 3.8.1 break语句 3.8.2 continue语句 3.9 问题发现与解决 3.9.1 五个常见错误 3.9.2 调试程序 3.10 C++类与vector类 3.11 总结 3.12 练习 复习题 第4章 函数一:基础 4.1 C++中的函数 4.1.1 只由一个main函数构成的程序 4.1.2 包含多个函数的程序 4.1.3 函数是个好东西 4.1.4 三个重要的问题 4.2 函数:基本格式 4.3 函数的编写要求 4.3.1 你想住在C++旅馆中吗 4.3.2 函数为先 4.3.3 函数声明或函数原型 4.3.4 函数定义、函数标题行与函数体 4.3.5 函数调用 4.3.6 传值调用 4.3.7 问题分析:未声明的标识符 4.4 重载函数 4.5 具有默认输入参数列表的函数 4.6 局部变量、全局变量和静态变量 4.6.1 局部变量 4.6.2 块范围 4.6.3 全局变量 4.6.4 危险的全局变量 4.6.5 问题分析:全局变量y0、y1与cmath 4.6.6 静态变量 4.7 C++stringstream类 4.8 总结 4.9 练习 复习题 第5章 函数二:变量地址、指针以及引用 5.1 数据变量和内存 5.1.1 sizeof运算符 5.1.2 预留内存 5.1.3 计算机内存和十六进制 5.2 取地址运算符& 5.3 指针 5.4 函数、指针以及间接运算符 5.4.1 解决思路 5.4.2 指针和函数 5.4.3 有效处理大型数据 5.5 函数和引用 5.5.1 复习:两种机制 5.5.2 为什么要强调指针的重要性 5.6 queue类 5.7 总结 5.8 练习 复习题 第6章 数组 6.1 使用单个数据变量 6.2 数组基础 6.2.1 数组的索引值从0开始 6.2.2 使用for循环和数组来实现的电话账单程序 6.2.3 数组的声明和初始化 6.2.4 数组越界==严重的问题 6.2.5 vector与数组的比较 6.3 数组和函数 6.3.1 每个数组都有一个指针 6.3.2 数组指针 6.3.3 向函数传递数组:最开始的引用调用 6.3.4 利用数组和函数生成随机数并进行排序 6.4 C字符串,也称为字符数组 6.4.1 字符数组的初始化 6.4.2 null字符 6.4.3 C字符串的输入 6.4.4 C++中提供的字符数组函数 6.5 多维数组 6.5.1 二维数组的初始化 6.5.2 嵌套的for循环和二维数组 6.5.3 利用二维数组来实现Bingo游戏 6.6 多维数组和函数 6.6.1 改进的Bingo卡片程序 6.6.2 白雪公主:利用二维数组来存储姓名 6.7 利用数据文件对数组赋值 6.8 总结 6.9 练习 复习题 第7章 类和对象 7.1 我们所了解的类和对象 7.2 编写自己的类 7.2.1 入门实例:自定义日期类 7.2.2 第一个C++类:Date类 7.2.3 揭开类的生命之谜 7.2.4 set和get函数的作用与VolumeCalc类 7.2.5 PICalculator类 7.3 作为类成员的对象 7.4 类的析构函数 7.5 对象数组 7.6 重载运算符与对象 7.7 指针、引用和类 7.7.1 指针和引用实例 7.7.2 处理日期和时间的程序实例 7.8 总结 7.9 练习 复习题 第8章 继承和虚函数 8.1 为什么继承如此重要 8.1.1 IceCreamDialog实例 8.1.2 Counter类实例 8.2 继承基础 8.2.1 Counter和DeluxeCounter实例 8.2.2 保护成员 8.2.3 员工、老板和CEO 8.3 访问控制符的规范和多继承 8.4 继承、构造和析构 8.4.1 构造函数和析构函数回顾 8.4.2 基类和派生类的默认构造函数——没有参数 8.4.3 在重载的构造函数中使用参数 8.4.4 基类和派生类的析构函数 8.4.5 医生也是人 8.4.6 关于派生类和基类构造函数的规则 8.5 多态和虚函数 8.5.1 多态——同一个接口,不同的行为 8.5.2 什么是虚函数 8.5.3 虚函数的作用 8.6 总结 8.7 练习 复习题 附录A 学习使用Visual C++2005Express Edition 附录B C++关键字表 附录C C++运算符 附录D ASCII码 附录E 位、字节、内存和十六进制表示 附录F 文件输入/输出 附录G 部分C++类 附录H 多文件程序 附录I Microsoft visual C++2005Express Edit
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值