1. C++基础
- 大多数编程语言通过两种方式来进一步补充其基本特征
1)赋予程序员自定义数据类型的权利,从而实现对语言的扩展
2)将一些有用的功能封装成库函数提供给程序员
1.1 变量和基本类型
-
算术类型
-
类型的转换
一个算术表达式既有无符号数又有int值时,int值就会转换成无符号数(负数总是加上模)
-
指定字面值的类型
变量提供了一个具名的、可供程序操作的存储空间。数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
在c++中,初始化和赋值时两个完全不同的操作。
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值替代。在C++ 11中,用花括号来初始化变量得到了全面应用。这种初始化的形式被称为列表初始化。当用于内置类型的变量时,列表初始化有一个重要特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显示地初始化,则其值由类确定。
-
命名规范
在变量使用的附近定义变量。这样更容易找到变量的定义,并且付给它一个比较合理的初始值。
左值引用——为对象起了另外一个名字,引用必须初始化。引用本身不是一个对象,因此一旦定义了引用,就无法令其再绑定到另外的对象。
一般初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序把引用和它的初值绑定在一起,而不是将初值拷贝给引用。
引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。指针本身是一个对象;指针无需在定义时赋值。
在新标准下,C++程序最好使用nullptr(字面值)来对空指针进行赋值,同时尽量避免使用NULL(预处理变量)。
建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr。
引用本身不是一个对象,因此不能定义指向引用的指针。但指针使对象,所以存在对指针的引用。
int i = 43;
int *p;
int *&r = p; // r是对指针p的引用,从右往左进行分析
r = &i;
*r = 0;
- const 引用
当一个常量引用被绑定到另外一种类型时,会绑定到一个临时量对象。C++将这种行为归为非法。
int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1 * 2;
错误 int &r4 = r1 * 2;
- const引用可能引用一个并非const的对象,此时允许通过其他途径改变其值。
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
错误r2 = 0;
- 编译器在编译过程中,将用到const变量的地方替换成对应的值,默认情况下,const对象被设定为仅在文件内有效。如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字。
- 顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。
顶层const 可以表示任意的对象时常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等符合类型的基本类型部分有关。
int i = 0;
int *const p1 = &i; // 不能改变p1,顶层
const in ci = 42; // 不能改变c1,顶层
const int *p2 = &ci; //允许改变p2,底层const
const int *const p3 = p2; // 顶层和底层
const int &r = ci; // 用于声明引用的const都是底层const
- 常量表达式是指值不会改变且在编译过程中能得到计算结果的表达式。
C++ 11规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为cosntexpr的变量一定是一个常量,而且必须用常量表达式初始化。
一个constexpr指针的初始值必须是nullptr,或者是存储于某个固定地址中对象。
函数体内定义的自动变量并非固定地址,不能用constexpr指针。定义于函数体外的对象其地址不变,可以用来初始化constexpr。 - constexptr把它所定义的对象置为了顶层const。
const int *p = nullptr; // p是一个指向常量的指针
constexpr int *q = nullptr; // q是一个常量指针
- 两种方法定义类型别名
1)typedef
2)using SI = Sales_item; // SI是Sales_item的同义词
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象时指向char的常量指针
错误的理解 const char *cstr = 0; // 是对const pstring cstr的错误理解,typedef定义后,就变成了不可分割的整体
- auto让编译器分析表达式所属的类型,使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中的所有变量的初始基本数据类型都必须一样。
1)使用引用其实使用的是引用的对象,真正参与初始化的是引用对象的值,因此编译器以引用对象的类型作为auto的类型
2)auto会忽略掉顶层const。同时底层const则会保留下来。
3)如果希望推断出的auto类型是一个顶层const,需要明确指出
4)设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留
int i = 0, &r = i;
auto a = r; //a是一个整数
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数,ci的顶层const被忽略
auto c = cr; // c是一个整数
auto d = &i; // d的是一个整型指针
auto e = &ci; //ci是一个指向整型常量的指针
const auto f = ci; //f是const int
auto &g = ci; //g是一个整型常量引用,绑定到ci
错误 auto &h = 42;
const auto &j = 42; //可以为常量引用绑定字面值
- decltype类型指示符
选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却并不计算表达式的值。
decltype与auto有些许不同
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x 是const int
decltype(cj) y = x; // y 是const int &, y绑定到x
- 如果表达式的内容十解引用操作,则decltype将得到引用类型。
变量如果加上括号,会得到引用类型。
decltype((variable)) 结果永远是引用,而decltype(variable)结果只有当variable本身是一个引用时才是引用。
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 结果是int
错误 decltype(*p) c; // c是int &,必须初始化
错误 decltype((i)) d; // d是int&,必须初始化
decltype(i) e; //e是int
1.2 字符串、向量和数组
- 作用域操作符::,编译器从操作符左侧名字的作用域中寻找右侧那个名字。
- 头文件一般不应该使用using
-
string初始化
1)拷贝初始化
使用=,执行的是拷贝初始化,编译器将等号右侧的初始值拷贝到新创建的对象中去。
2)直接初始化
不是用等号 -
string的操作
- string::size_type是无符号类型的值。注意不要在表达式中混用带符号树和无符号数。
- 标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方可以使用这两种字面值来代替。当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加号运算符(+)的两侧的运算对象至少有一个是string。
- 为了与C兼容,C++中字符串字面值并不是标准库类型string对象。字符串字面值与string是不同类型。
-
处理string对象中的字符
range for可以处理string中的每个字符。
-
vector——类模板
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化。
老式的声明vector<vector<int> >,需要添加一个空格,C++11不需要。
-
圆括号和花括号的区别,花括号是列表初始化:
- 范围for语句体内不应改变其所遍历序列的大小。
-
vector操作
-
迭代器
有效迭代器指向某个元素或者指向容器中尾元素的下一位置,其他情况都属于无效。
- 标准库容器的迭代器都定义了==和!=,大多数没有定义<。
- 使迭代器失效的操作
1)不能在范围for循环中向vector对象添加元素
2)任何一种可能改变vector对象容量的操作,都会使vector对象迭代器失效 -
迭代器运算
距离的类型名是difference_type的带符号整型数。
- 数组 —— 跟C语言一样
size_t在cstddef头文件里面进行了定义。
指针也是迭代器 - 在iterator头文件中定义了begin和end函数,end函数返回尾元素下一位置的指针。尾后指针不能执行解引用和递增操作。
两个指针相减的结果类型是ptrdiff_t。
int *pbeg = begin(arr), *pend = end(arr);
while (pbeg != pend && *pbeg >= 0) {
++pbeg;
}
尽管C++支持C风格字符串,但在C++最好还是不要使用它们。因为C风格字符串使用不方便,且极易引发程序漏洞。
现代C++程序应该尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。多维数组
int ia[3][4];
for (auto p = begin(ia); p != end(ia); ++p) {
for (auto q = begin(*p); q != end(*p); ++q) {
cout << *q << ' ';
}
cout << endl;
}
1.3 表达式
- 重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的,但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
- 左值和右值:当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)。
如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型,例如p是int 类型,decltype(p)是int &。 - 只有四种运算符明确规定了运算对象的求值顺序:&& || ?: ,四种运算符
- 求值顺序、优先级、结合律
1)当拿不准时候,最好用括号来强制让表达式的组合关系符合程序逻辑的要求
2)如果改变了某个运算符对象的值,在表达式的其他地方不要使用这个运算对象。例外:当改变运算符的子表达式就是另外一个子表达式的运算对象时。例如:*++iter - c++新标准规定商一律向0取整(即直接切除小数部分)
- 除非必要,否则不用递增递减运算符的后置版本
- 在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针;数组用作decltype、&、sizeof、typeid的运算对象时除外。
- 四种类型的显式转换
cast-name<type>(expression)
1)static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。
2)dynamic_cast
支持运行时类型识别
3)const_cast
只能改变运算对象底层const
4)reinterpret_cast
为运算对象的位模式提供较低层次上的重新解释。
const char *pc;
char *p = const_cast<char *>(pc); // 正确,但是通过p写值是未定义的行为
- 避免强制类型转换,尤其是reinterpret_cast
在有重载函数的上下文中使用const_cast无可厚非
-
运算符优先级表
1.4 语句
- break语句负责终止离它最近的while、do while、for或switch语句,并从这些语句之后的第一条语句开始继续执行。
continue终止最近的循环中的当前迭代并立即开始下一次迭代。 - 异常处理机制为程序中异常检测和异常处理两部分的协作提供支持。
1)throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。
2)try语句块,异常处理部分使用try语句块处理异常。try语句块中代码抛出的异常通常会被某个catch子句处理。
3)一套异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。 - 当异常被抛出时,首先搜索抛出该异常的函数。如果没有找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推。沿着程序的执行路径逐层回退,直到找到适当类型的catch子句为止。如果最终没有找到任何匹配的catch子句,程序转到terminate的标准库函数。
-
异常类分别定义在4个头文件中
1)exception头文件定义最通用的异常类。只报告异常的发生,不提供任何额外的信息
2)stdexcept头文件定义了几种常用的异常类
3)new头文件定义了bad_alloc异常类型
4)type_info定义了bad_cast异常类型 - 只能以默认初始化的方式初始化exception bad_alloc bad_cast对象,不允许为这些对象提供初始值。
其他异常类型的行为恰好相反:应该使用string对象或C风格字符串初始化这些类型的对象,不允许使用默认初始化方式。
异常类型之定义了一个what成员函数,返回const char*,提供异常的一些文本信息。
1.5 函数
- 在C++中,名字有作用域,对象有生命周期。
- 函数的形参尽量使用常量引用,如果将其定义为普通引用,就不能将const 对象、字面值或者需要类型类型转换的对象传递给普通的引用形参。
-
编写能处理不同数量实参的参数,C++ 11新标准提供了两种主要的方法
1)如果所有的实参类型相同,可以传递一个名为intializer_list的标准库类型
initializer_list对象中的元素用于是常量值,无法改变。
2)如果实参类型不同,可以编写可变参数模板
void error_msg(initializer_list<string> il) {
for (auto beg = il.begin(); beg != il.end(); ++beg) {
cout << *beg << " ";
}
cout << endl;
}
- vargargs省略符形参应该仅仅用于C和C++通用的类型。特别注意,大多数类型的对象在传递给省略符形参时都无法正确拷贝。
- 返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 返回引用的函数得到左值,其他返回类型得到右值。
- C++11规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。
vector<string> process() {
if (expected.empty()) {
return {};
} else if (expected == actual ) {
return {"functionX", "okay"};
} else {
return {"functionX", expected, actual};
}
}
- C++ 11可以使用尾置返回类型来简化复杂返回类型的函数声明
auto func(int i) -> int (*) [10]; // func接受一个int类型的实参,返回一个指针,该指针指向还有10个整数的数组
- 重载函数:同一作用域内的几个函数名字相同但形参列表不同。编译器会根据传递的实参类型推断想要的是哪个函数。
不允许两个函数除了返回类型外其他所有要素都相同。 - 顶层const 不影响传入函数的对象,所以无法和没有顶层const的形参区分开来;
底层const可以重载,此时形参是某种类型的指针或引用,当传递一个非常量对象或指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
顶层const 可以表示任意的对象时常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等符合类型的基本类型部分有关。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明
Record lookup(Account &);
Record lookup(const Account &); // 新函数
Record lookup(Account*);
Record lookup(const Account *); // 新函数
- const_cast和重载:将const转变为普通的无const
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const_string&>(s2));
return const_cast<string&>(r);
}
- 函数匹配(重载确定):
1)编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
2)找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
3)有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用。 - 若在内层作用域中声明重载函数,它将隐藏外层作用域中声明的同名实体。编译器首先在当前作用域寻找,如果找到相应的函数,编译器就会忽略掉外层作用域中的同名实体,剩下的工作就是检查函数调用是否有效。
void print(const string&);
void print(double);
void fooBar(int ival) {
void print(int); //隐藏了外层作用域的两个print
print("value: "); //错误
print(ival); //正确
print(3.14); //调用print(int)
}
正确做法:
void print(const string&);
void print(double);
void print(int);
void fooBar(int ival) {
print("value: "); //print(const string&)
print(ival); //print(int)
print(3.14); //print(double)
}
- 默认实参
一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。 - 内联函数
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。 - constexpr函数
能用于常量表达式的函数,要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr函数不一定返回常量表达式
constexpr int new_sz() { return 42;}
constexpr size_t scale(size_t cnt) {
return new_sz() * cnt;
}
int arr[scale(2)]; // 正确
int i = 2;
int a2[scale(i)]; // 错误:scale(i)不是常量表达式
- 调试帮助
程序可以包含一些用于调试的代码,但是这些代码只是在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。
1)assert预处理宏(cassert头文件)
assert(expr); 如果表达式为假(即0),assert输出信息并终止程序的执行。如果为真,则什么也不做。
预处理名字由预处理器而非编译器管理。
2)NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
可以使用编译命令选项-D来定义NDEBUG。
可以将assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。
预处理器定义对于程序调试很有用的名字:
__func__ 函数名字
__FILE__ 文件名
__LINE__行号
__TIME__编译时间
__DATE__日期
- 函数匹配
1)函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
2)第二步考察本次调用提供的实参,然后从候选参数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
如果没有找到可行函数,编译器将报告无匹配函数的错误。
3)第三步是从可行函数选择与本次调用最匹配的函数。实参类型与形参类型越接近,它们匹配得越好。
如果没有最匹配的函数,则该调用错误。
调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。 -
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
void ff(int);
void ff(short);
ff('a'); // char提升成int;调用f(int)
void manip(long);
void manip(float);
manip(3.14); // 错误:二义性调用
Record lookup(Account &);
Record lookup(const Account&);
const Account a;
Account b;
lookup(a); // 调用lookup(const Account &)
lookup(b); // 调用lookup(Account &)
- 函数指针
函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
1)函数或者函数指针都可以作为形参,函数类型实际上被当成了指针使用
2)作为返回类型时,编译器不会自动将函数返回类型当成对应的指针类型处理。
using F = int(int*, int); // F是函数类型,不是指针
using PF = int(*)(int*, int); //PF是指针类型
PF f1(int); // 正确
F f1(int); // 错误,不能返回函数类型
F *f1(int); //正确
上面等价于下面的直接声明:
int (*f1(int))(int*, int);
auto f1(int) -> int (*) (int*, int);
- decltype作用于某个函数时,它返回函数类型而非指针类型。
string:size_type sumLength(const string&, const string&);
string:size_type largerLength(const string&, const string&);
//下面的函数返回上面两个函数的指针的一个
decltype(sumLength) *getFcn(const string&); // decltype返回的是函数类型
1.6 类
- 类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。封装实现了类的接口和实现的分离。
- 成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
- 把const关键字放在成员函数的参数列表之后,表示this是一个指向常量的指针。默认情况下,this的类型是指向类类型非常量版本的常量指针。因此常量成员函数不能改变调用它的对象的内容。
std::string isbn() const {} // 将this声明成 const Sales_data *const,指向常量对象
- 编译器分两步处理:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
- 函数如果定义在类的内部,默认是内联的,如果定义在类的外部,默认是不内联的。
- 一般来说,当定义的函数类似于某个运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了保持一致,combine必须返回引用类型。
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
- 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
IO类型属于不能被拷贝的类型,因此只能通过引用来传递。读取和写入的操作会改变流的内容,所以是普通引用。
istream &read(istream &is, Sales_data &item) {
double price = 0;
...
return is;
}
- 如果类没有显式地定义构造函数,那么编译器就会隐式地定义一个默认构造函数——合成的默认构造函数。
1)如果存在类内初始值,用它来初始化成员
2)否则,默认初始化该成员。 - 某些类不能依赖合成的默认构造函数
1)没有声明任何构造函数是才自动生成
2)合成的默认构造函数可能执行错误的操作,利于默认初始化的值是未定义
3)不能为某些类合成默认的构造函数。类中含有一个其他类类型的成员,且这个成员的类型没有默认构造函数。 - 定义默认构造函数
1)不接受任何实参,所以是一个默认构造函数
2)C++11中,如果需要默认行为,可以在参数列表后加上= default
Sales_data() = default;
- 尽管编译器能够合成拷贝、赋值和销毁操作,但是对于某些类来说合成版本无法正常工作。特别是,当类需要分配类对象之外的资源时。
- 友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或函数称为它的友元。
友元声明只能出现在类定义的内部,即以friend关键字开头的函数声明。
- 可变数据成员mutable
不会是const,即使它是const对象的成员。一个const成员函数可以改变可变成员的值。
- 类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式。
- 一个常量成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
- 通过区分成员函数是否是const,可以对其进行重载。
- 在实践中,设计良好的C++代码常常包含大量的小函数。
- 不完全类型:声明之后定义之前都是不完全类型
不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
- 友元关系不存在传递性。
- 要想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系
1)首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen
2)接下来定义Screen,包括对于clear的友元声明
3)最后定义clear,此时才可以使用Screen的成员
class Screen{
friend void Window_mgr::clear(ScreenIndex);
};
- 对于定义在类内部的成员函数,解析其中名字的方式:
1)首先编译成员的声明
2)直到类全部可见后才编译函数体 - 成员函数中使用的名字按照如下方式解析:
1)首先,在成员函数内查找该名字的声明。只有在函数使用之前出现的声明才被考虑
2)如果在成员函数内没有找到,则在类内继续查找,类的所有成员都可以被考虑
3)如果类内没有找到该名字的声明,在成员函数定义之前的作用域内继续查找
- 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。
建议使用构造函数初始值,而非赋值。 - 最好令构造函数初始值的顺序与成员声明的顺序保持一致。
- 委托构造函数,一个委托构造函数使用它所属类的其他构造函数执行自己的初始化过程,或者说它把自己的一些或者全部职责委托给了其他构造函数。
class Sales_data {
public:
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt *price) {}
Sales_data(): Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0, 0) {}
Sales_data(std::istream &is): Sales_data() {read(is, *this);}
};
- 在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
- 转换构造函数:通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
编译器只会自动执行一步类型转换。
explicit抑制构造函数定义的隐式转换,并且只能在类内声明构造函数时使用explicit,在外部定义时不应重复。
- 聚合类——struct,所有成员都是可以直接访问的
-
字面值常量类
- 类的静态成员:需要一些成员与类本身直接相关,而不是与类的各个对象保持联系。
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。
静态成员函数不包含this指针,不能声明成const。
static只出现在类内部的声明语句中。
要想确保对象只定义次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一文件中。
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
静态成员可以是不完全类型;静态成员可以作为默认实参,非静态成员不可以。