六章 类型与声明
-
确保程序中没有不确定的或未定义的行为
-
在算术逻辑表达式和位逻辑表达式中,bool被自动转化为int
-
{}初始化器列表可防止窄化转换的发生
-
使用指定类型声明时最好使用{}初始化器语法
-
使用auto声明时最好用=语法
-
decltype类型推断用法:
template<class T, class U>
auto operator+(const Matrix
& a, const Matrix & b) -> Matrix<decltype(T{} + U{})>;
七章 指针、数组、引用
-
优先使用nullptr而不是NULL、0
-
使用static_cast 显示转换void* 指针到某个类型,避免奇怪的错误
-
避免在接口中使用数组
-
不要使用多维数组,用合适的容器替代
-
用资源句柄如string、vector、unique_ptr控制自由存储上数组的生命周期
-
字符串字面值常量的类型是”若干个const字符组成的数组”,以’\0’结尾
char* p =”plato”;//错误
char p[] =”plato”;//正确
-
原始字符串:”( 和 )”是分隔符,在)之后和(之前的序列必须完全一致;适用于正则表达式
R”(ccc)”
R”**(ccc)))ccc)*”
-
L”ccc”,宽字符字符串,类型const wchar_t[];LR”(ccc)”宽字符原始字符串;以L’\0’结尾
-
六种字符字面值常量支持unicode:
“folder\\file” //基于实现字符集的字符串
R”(folder\file)” //基于实现字符集的原始字符串
u8”folder\\file” //UTF-8字符串
u8R”(folder\file)” // UTF-8原始字符串
u”folder\\file” //UTF-16 字符串
uR”(folder\file)” //UTF-16 原始字符串
U”folder\\file” //UTF-32 字符串
UR”(folder\file)” //UTF-32 原始字符串
-
constexpr: 编译时求值,const: 值不发生改变
-
前置const使指针所指对象成为常量,使用*const使指针本身变为常量
-
提供给”普通”T&的初始值必须是T类型的左值(能获取地址的对象,即有身份的)
有身份的:程序中有对象的名字,或指向对象的指针,或该对象的引用。(左值)
可移动的:能把对象的内容移出来。(右值)
-
const T&的初始值不一定非得是左值,甚至可以不是T类型的,此时
[1]首先,如果有必要的话执行目标为T的隐式类型转换
[2]然后,所得的值置于一个临时变量中
[3]最后,把这个临时变量作为初始值
-
右值引用可以绑定到右值,但是不能绑定到左值。声明符&&表示右值引用,没有const 右值引用
八章 结构、联合、枚举
-
可以声明一对同名的struct和非struct,但是应避免使用同名实体
-
struct可以包含成员函数
-
pod类型的优化,is_pod
::value -
域:指定成员所占的位数,可以把它定义成域(struct 成员):
struct PPN{
unsigned int PFN : 22;
int : 3;
bool dirty : 1;
}
域必须是整型或枚举类型,无法获取域的地址。(并不能节省空间,少用为妙)
-
不要将union用于显示类型转化,应使用reinterpret_cast
-
避免使用union
-
enum class :它的枚举值名字位于enum的局部作用域内,枚举值不会隐式转换成其他类型(建议使用)
enum class Color{red, black, green};
-
普通enum,它的枚举值名字与枚举类型本身位于同一个作用域,枚举值隐式转换成整数
-
枚举类型的基础类型默认为int,可以显示指定:
enum class Warning : int{green,yellow, orange, red};//定义
允许先声明后定义:
enum class Color : char;
enum class Color : char{red, yellow};
-
普通enum:
enum Warning {green, yellow, orange, red};
Warning a1 = 7; //错误,不存在int到Warning的类型转换
int a2 = green;// OK,隐式转换成int
如果制定了基础类型,声明和定义则可以分开,否则不能分开,此时枚举类型的基础类型是推算出来的:
如果枚举值都是非负值,则范围是[0, 2的k次方-1];否则[-2的k次方,2的k次方-1],k是保证所有枚举值都在这个范围的最小值。
-
未命名的enum用于声明一组整型常量
九章 语句
-
直到有合适的初始值再声明变量
-
在if语句中,一个分支声明的名字不能在另一个分支直接使用。if语句的一个分支不能仅有一个声明语句,没有别的语句;如果想在一个分支中引入一个新名字,则该声明语句必须包含在块中。如:
void f(int i){
if(i){
int x = i +2;
++x;
}else{
++x;//错误,在作用域之外
}
++x;//错误,在作用域之外
if(i)
int x = i + 2; //错误,if语句分支的声明
}
-
可以在switch语句的块内声明变量,但是不能不初始化
-
条件中声明,只能声明并初始化一个变量或const
if(double d = prim(true)){
left /= d;
break;
}else{
right /= d;
}
d 的作用域从声明处开始,到条件控制的语句结束为止。d在if,else两个分支中有效。
-
范围for循环:
int sum(vector
& v){ int s = 0;
for(int x : v)
s+=v;
return s;
}
冒号之后的表达式必须是一个序列,即v.begin()/v.end()或者begin(v)/end(v),得到的是一个迭代器
「1」 编译器首先尝试寻找并使用begin和end成员,如果找到了begin和end,但是他们不能表示一个范围,则范围for是错误的
「2」 如果没有找到,则编译器继续在外层作用域查找begin/end成员,如果找不到或者找到的不能用,则范围for是错误的
使用引用可以修改值且避免拷贝: for(int& x : v){}
-
应尽量避免使用do{}while循环语句
-
标签的作用域是标签所处的函数,避免使用goto语句
-
良好的注释风格
十章 表达式
-
注意临时变量的销毁
-
constexpr : 编译时求值,必须用常量表达式初始化
-
以常量表达式初始化的const可以用在常量表达式中,与constexpr不同,const可以用非常量表达式初始化,此时该const不能用作常量表达式
-
符号化常量
-
含有constexpr构造函数的类称为字面值常量类型,构造函数必须足够简单才能声明为constexpr,简单的含义是它的函数体必须为空且所有成员都是用潜在的常量表达式初始化的
-
对于成员函数来说constexpr隐含了const
-
constexpr是一个关于值的概念,constexpr函数可以接受const引用参数(值的概念)
-
字面值常量类型允许类型丰富的编译时程序设计
-
地址常量表达式:全局变量等静态分配的对象的地址是一个常量,但该地址是由连接器赋值而非编译器,编译并不知道这类地址常量的值到底是多少,这限制了指针或者引用类型的常量表达式的使用范围,如;
constexpr const char* p1 = “asdf”;
constexpr const char* p2 = p1; //OK
constexpr const char* p2 = p1 +2; // 错误:编译器不知道p1本身的值是多少
constexpr const char c = p1[2]; //OK, c == ‘d’, 编译器知道p1所指的值
-
转换某个值的类型,如果能够再转换为原类型,且保持初始值不变的,称该转换是值保护的,否则就发生了窄化类型转换
-
如果窄化转换无法避免,考虑使用运行时执行检查的类型转换函数narrow_cast<>()
-
指向函数的指针和指向成员的指针不能隐式转换成void*
十一章 选择适当的操作
-
条件表达式c?e1:e2, e1 e2的类型必须相同,或者他们都能隐式转换成同一类型T
-
条件表达式可以用在常量表达式中
-
throw表达式可以作为条件表达式的一个分支:
int *p = 1;
int i = p? *p : std::runtime_error{“unexpected error”};
-
内存管理问题:内存泄漏,提前释放,重复释放
-
new的初始化器与正常初始化一样,可以使用{}或者()
-
避免使用自由存储,如果无法避免,使用管理器对象管理,如string,vector,unique_ptr, shared_ptr.
-
很多习惯使用自由存储的场合其实都可以用移动语义替代,只要从函数中返回一个表示大对象的管理器对象就可以了。
-
关于new和delete,尽量确保没有裸new,即,令new位于构造函数或类似的函数中,delete位于析构函数
-
RAII,资源获取即初始化,是一项避免资源泄漏的基本技术
-
获取内存空间
void* operator new(size_t);
void operator delete(void* p);
void operator new[] (size_t)
void operator delete[] (void* p);
上面的分配和释放函数负责处理无类型且未初始化的内存(原始内存),而非类型明确的对象。无类型的内存层和带类型的对象层的映射关系由运算符new和delete负责。运算符new调用operator new分配空间。
-
当内存不足时,默认抛出bad_alloc异常
-
放置式new
Void* operator new (size_t sz, void* p) noexcept;
void* operator new[] (size_t sz, void* p)noexcept;
void operator delete (void *p, void* p) noexcept;
void operator delete[] (void*p, void*) noexcept;
使用实例:
X* p2 = new(buf) X;
放置式delete可能会告知垃圾收集器当前删掉的指针不再安全,除此之外什么也不做。
放置式new用于从某一特定区域分配内存示例:
class Arena{
Public:
virtual void* alloc(size_t sz) = 0;
virtual void free(void*p) = 0;
};
void* operator new (size_t sz, Arena* a){return a->alloc(sz); }
extern Arena* Persitent;
extern Arena* Shared;
void g(int i){
X* p = new(Persitent)X(i);//在某持续性存储上分配X
X* p = new(Shared) X(i);//在共享内存上分配X
}
把对象置于一块标准自由存储管理器不(直接)控制的区域,意味着我们在销毁此类对象时必须特别小心。处理这一问题的常规做法是显示调用一个析构函数:
void destroy(X* p, Arena*){
p->~X();
a->free(p);
}
-
nothrow new
如果程序不允许出现异常,可以用nothrow 版本的new和delete:
void f (int n){
int *p = new(nothrow) int[n];
if(p ==nullptr){
//处理内存错误
}
operator delete(nothrow, p);
}
实现细节位于<new> :
void* operator new(size_t sz, const nothrow_t &) noexcept;//如果分配失败,返回nullptr
void operator delete(void* p, const nothrow_t&) ;noexcept;
void* operator new[] (size_t sz, const nothrow_t&) noexcept;//如果分配失败,返回nullptr
void operator delete[] (void* p, const nothrow_t&) ;noexcept;
-
未限定列表:当明确知道所用类型时,可以使用未限定类表,使用场景:
- 函数实参
- 返回值
- 赋值运算符右侧运算对象
- 下标
只有当列表的所有元素类型相同时,才能推断列表的类型:
auto x0 = {}//错误
auto x4 = {1, 2.0}//错误
无法通过推断未限定列表的类型使其作为普通模版的实参:
template
void f(T);
f({}) //错误,初始化器的类型未知
f({1,2,3})//错误,未限定的列表与”普通的T”不匹配
类似的,当容器的元素类型时模版时,无法推断:
template
void f2(const vector
&); f({1,2,3})//错误,无法推断T
f({“Kona”, “Sidney”})//错误,无法推断T
-
限定列表
使用限定列表构建对象与直接初始化规则相同:
T x{v};
-
{}列表的实现模型
- 如果列表被用作构造函数的实参,则其实现过程与使用()列表类似,除非列表的元素以传值的方式传给构造函数,否则不会拷贝列表的元素
- 如果{}列表用于初始化一个聚合体(一个数组或一个未提供构造函数的类)的元素,则列表的每个元素分别初始化聚合体的一个元素,除非列表的元素以传值的方式传给聚合体元素的构造函数,否则不会拷贝
- 如果{}列表用于构建一个initializer_list对象,则列表的每个元素分别初始化initializer_list底层数组的一个元素;通常,把元素从initializer_list拷贝到实际使用他们的地方
如:
Vector double v = {1,2,3.14};
vector 含有一个接受初始化器列表的构造函数,其构造过程和用法如下:
const double temp[] = {double(1), double(2), 3.14};
const initializer_list
tmp {temp, sizeof(temp)/sizeof(double)}; vector
v(tmp); {}列表的底层数组不可修改,意味着接受列表元素的容器必须使用拷贝操作,而不能使用移动操作。
-
Lambda 表达式,组成:
- 捕获列表[],可能为空,指明定义环境中的哪些名字可用于lambda表达式内,是拷贝还是引用
- 可选的参数列表()
- 可选的mutable修饰符,指明lambda可能会修改它自身的状态,即通过值捕获的变量的副本
- 可选的noexcept修饰符
- 可选的->形式的返回类型声明
- 表达式体{}
把lambda表达式看作是一种定义并使用函数对象的便捷方式
捕获方式:
-
[]:空
-
[&]:全部按引用
-
[=]:全部按值
-
[捕获列表]:以&前缀的按引用,其他按值。捕获列表可以出现this,或者紧跟…以表示元素
-
[&, 捕获列表]: 捕获列表按值,其他按引用。列出的名字不能带&
-
[=, 捕获列表] : 捕获列表按引用,其他按值,列出的名字必须带&
捕获可变模版实参:
template<typename… Var>
void algo(int s, Var… v){
auto helper = [&s, &v…]{return s*(h1(v…) + h2(v…));}
}
要注意lambda的生命周期,避免引用到已经不存在的变量。
lambda 与this:
class Request{
function<map<string, string>(const map<string, string>&)> oper;
map<string, string> values;
map<string, string> results;
Request(const string& s);
void execute(){
[this] () {results = oper(values);}//成员通过引用方式捕获,因此需注意多线程环境的竞争问题
}
}
默认情况下,lambda生成的operator()()是一个const,如果要改变自身状态,需声明为mutable:
[] () mutable{}
参数可忽略,最简形式:[]{}
labmda表达式的类型可推断得到:
-
如果body不包含return语句,则返回类型是void;
-
如果包含一条return语句,返回类型是该return表达式的类型;
-
其他情况必须显示提供返回类型,如:
auto z2 = [=, y] {if (y) return 1; else return 2;}//错误,body过于复杂,无法推断
auto z2 = [=, y] ()->int{if(y) return 1; else return 2;}//OK,显示的返回类型
无法在推断出一个auto变量的类型之前使用它:
auto rev = &rev { if (1<e−b) { swap(*b,*–e); rev(++b,e); } }; // error
应该先引入一个新的名字:
void f(string& s1, string& s2) {
function<void(char∗ b, char∗ e)> rev = & { if (1<e−b) { swap(∗b,∗−−e); rev(++b,e); } };
如果lambda什么也不捕获,可以将它赋值给一个指向正确类型函数的指针:
double (∗p1)(double) = [] (double a) { return sqrt(a); }; double (∗p2)(double) = [&] (double a) { return sqrt(a); }; //** error: 捕获了内容 double (∗p3)(double) = [] (int a) { return sqrt(a); }; //** error: 参数类型不匹配
-
显示类型转换
-
构造,{},防止窄化转换
-
命名转换:令类型转换的含义更明显,让程序员有机会表达他们的真实意图
- const_cast:对某些声明为const的对象获得写入的权利
- static_cast:执行关联类型之前的转化
- reinterpret_cast:处理非关联类型的转化
- dynamic_cast:执行指针或者引用向类层次体系的类型转换,并执行运行时检查
-
C风格转换
(T)e; 可以执行static_cast,reinterpret_cast,const_cast任意组合之后得到的类型转化,避免使用
-
函数式符号
T(e);对内置类型来说,T(e) 等价于 (T)e,避免使用
-
-
narrow_cast 模版
template<class Target, class Source>
Target narrow_cast(Source v) {
auto r = static_cast
(v); if (static_cast(r)!=v) // convert the value to the target type throw runtime_error(“narrow_cast<>() failed”); return r;
}
对于数字类型之间的转换,考虑使用执行运行时检查的narrow_cast(标准库没有定义)
-
建议用T{v}处理行为良好的构造,用命名转换处理其他任务
-
与后置++相比,优先使用前置++
十二章 函数
- 把函数视作代码的一种结构化机制
- 待续
十六章 类
-
将概念表示为类,接口与实现分离
-
默认拷贝语义: 逐成员复制
-
优先使用{}初始化:Data today {21,10,2019};
-
默认值必须在参数的可能值集合之外;或者使用默认值作为默认参数
-
默认情况下应该将单参数构造函数声明为explicit以禁止隐式类型转换,在类外定义中不能重复explicit
-
判断显示和隐式的方式是看初始化器带不带类型,如:
void my_func(Data d);
Date date = Data{15}; //显示初始化
Date data = {15}; //隐式初始化
my_func({15}); //隐式初始化
my_func(Date{15});//显示初始化
-
类内初始化器
class Date{
int d{today.d};
int m{today.m};
int y{today.y};
};
-
常量成员函数指出不会修改对象的状态,隐含const this指针
class Data{
int d,m,y;
public:
int day() const {return d;}
};
类外定义的常量成员函数,必须使用const 后缀
const和非const对像都可以调用const 成员函数;
非const成员函数只能被非const对象调用
-
将一个类成员定义为mutable,表示即使是在const对象中也可以修改此成员
-
static成员的定义中不要重复关键字static
-
嵌套类可以访问所属类成员,包括private成员,但没有所属类当前对象的概念;
一个类并没有任何特殊权限能访问其嵌套类的成员。
-
使用名字空间建立类和其辅助函数的显示关联
-
优先使用具体类
十七章 构造、清理、拷贝、移动
-
拷贝和移动的区别:拷贝后两个对象具有相同的值,而移动后,移动源不一定具有原始值
-
应该使用构造函数建立”类不变式”,所谓类不变式就是当成员函数被调用时必须保持的某些东西
-
构造函数执行顺序:基类构造函数 ==> 成员构造函数 ==> 自身函数体;析构函数执行顺序相反
构造函数按声明顺序执行成员和基类的构造函数,而非初始化器的顺序。
-
对象离开作用域,或者自由存储上的对象调用delete,会隐式的调用析构函数
-
构造、析构函数都可以声明为private或delete来阻止构造或析构
-
含有虚函数的类应该声明virtual 析构函数
-
初始化一个无构造函数的类的对象的方法:
【1】逐成员初始化
【2】拷贝初始化
【3】默认初始化(不用初始化器,或空初始化列表)
struct Work{
string author;
string name;
int year;
};
Work s9{“author”, “name”, 1842}; //’‘【1】
Work s2(s9);// 【2】
Work none{}// 【3】
使用{}默认初始化的效果是对每个成员进行初始化;
如果没有可接受参数的构造函数,可以省略{}:
Work alpha;
Void f(){
Work beta;
}
对静态分配的对象,这种初始化与使用{}完全一样,但对局部变量和自由存储空间对象,只对类类型的成员进行默认初始化,内置类型的成员不进行初始化。如果希望保证局部变量初始化,可提供初始化器,如{}。
只有能访问成员时,逐成员初始化才有效,如果一个类有私有的非static数据成员,则需要一个构造函数来进行初始化
-
使用()语法,可以请求在初始化过程中使用一个构造函数,即可以保证用构造函数进行初始化而不是{}语法也提供的逐成员初始化和初始化器列表初始化。
-
接受单一std::initializer_list参数的构造函数被称为初始化器列表构造函数。
- 如果默认构造函数和初始化器列表构造函数都匹配,优先选择默认构造函数
- 如果初始化器列表构造函数和一个普通构造函数都匹配,优先选择初始化器列表构造函数
-
Initializer_list
是一个不可改变的序列,可以用begin(), end(), size()进行访问 -
引用和const必须被初始化,因此一个包含这些成员的类不能默认构造,除非程序员提供了类内成员初始化器或定义了一个默认构造函数来初始化他们。
-
一个类型的数组或容器可以分配一组默认初始化的元素,该类需要一个默认构造函数:
struct S1{S1();};
struct S2{S2(string);}
S1 a1[10]; // 正确
S2 a2[10];//错误,无默认构造函数
Vector
v1(10);//正确 Vector
v2(10);//错误,无默认构造函数,不能初始化 -
成员初始化列表以冒号开头,后面的成员初始化器用逗号间隔
-
派生类的基类的初始化方式与成员初始化一样,即,如果基类要求一个初始化器,我们就必须在构造函数中提供相应的基类初始化器。17.4.2有个参考例子
-
使用成员风格的初始化器,但用的是类自身的名字,它会调用另一个构造函数作为构造过程的一部分。这样的构造函数称为委托构造函数(delegating),也称为转发构造函数(forwarding constructor),如:
class X{
int a;
Public:
X(int x){a =x;}
X():X{42}{}
X():X{42},a{56}{} //错误
};
不能同时显示和委托初始化一个成员
-
类内初始化器:可以类声明中为非static数据成员指定初始化器,可以使用{}和=语法,不能使用()语法:
calss A{
int a{7};
int a = 7;};
一个类内初始化器可以使用它的位置(在成员声明中)所在作用域的所有名字
-
一般来说static成员在类外定义。
-
在类内声明中初始化static成员的情况:static成员必须是整型或是枚举类型的const,或字面值类型的constexpr,且初始化器必须是一个常量表达式。
-
设计移动操作时不要让他抛出异常,并令源对象处于可析构(会调用析构函数销毁)和可赋值状态
-
拷贝:
拷贝构造函数:X(const X&)
拷贝赋值运算符: X& operator=(const X&)
两者的区别是前者初始化一片未初始化的内存,而后者必须处理目标对象已构造并可能拥有资源的情况
-
编写拷贝操作时,确保拷贝了每个成员和基类
-
拷贝操作必须满足两个准则
- 等价性
- 独立性
-
防止切片:
- 禁止拷贝基类:delete基类拷贝操作
- 防止派生类指针转化为基类指针:将基类声明为private或protected基类
-
移动赋值背后的思想是将左值的处理与右值的处理分离。拷贝接受左值,移动接受右值。对于return值,采用移动构造函数。
- 移动构造: X(X&&)
- 移动赋值:X& operator=(X&&),非const右值,可修改参数
-
少数情况下,如返回值,语言规则指出编译器可以使用移动操作,但是,一般情况下必须传递右值引用告知编译器。如std::move.
-
编译器会默认生成移动操作
-
编译器默认生成函数的规则:
- 如果一个类声明了任意构造函数,那么编译器不会为该类生产默认构造函数
- 如果程序员为一个类声明了拷贝操作、移动操作、或析构函数,则编译器不会为该类生成拷贝操作、移动操作或析构函数,这里应该理解成只要声明了其中一个,其他都不会生成。如果需要使用默认定义,则用 =default显示声明默认操作。
-
默认操作的效果:逐成员拷贝,逐成员默认构造,逐成员移动;如果移出对象是内置类型,其值保持不变。内置类型的”默认构造”不会进行初始化。
-
=delete
-
显示删除编译器默认生成的函数,参考23.
-
使用delete删除任何我们能声明的函数
template
T* clone(T* p){
return new T{*p};
}
Foo* clone(Foo*) = delete;//克隆Foo导致错误
-
删除不需要的类型转换
struct Z{
Z(double);
Z(int) = delete;
};
-
控制在哪里分配对象
class Not_on_stack{
~Not_on_stack() = delete; //无法声明一个不能被销毁的对象
};
class Not_on_free_store{
void *operator new(size_t) = delete;//删除类的内存分配运算符,无法在自由空间分配对象
};
-
-
优先选择移动语义而不是浅拷贝;小心纠缠的数据结构,即类中含有资源对象的指针。
十八章 运算符重载
-
无法重载的运算符
-
:: 作用域解析
-
. 成员选择
-
.* 通过指向成员的指针访问成员
这3种运算接受一个名字而非一个值作为第二个运算对象
-
sizeof
-
alignof
-
typeid
-
?: 条件表达式
-
-
二元运算符和一元运算符:
- 二元运算符: aa@bb, 可以理解成aa.operator@(bb)或者operator@(aa,bb),如果两个都定义了,由重载解析决定使用哪一个
- 一元前置运算符: @aa, aa.operator@() 或者 operator@(aa),如果都定义了,由重载解析决定
- 一元后值运算符:aa@, aa.operator@(int)或者operator@(aa, int),如果都定义了,由重载解析决定
-
声明运算符时,必须确保它的语法与C++标准的规定一致,如无法定义一元的%或者三元的+
-
运算符函数应该是成员函数或至少接受一个用户自定义类型的参数(重定义new和delete不满足该规则)
-
如果某个运算符函数接受一个内置类型作为它的第一个参数,那么该函数不能是成员函数
-
可以根据运算对象(参数)的类型找到名字空间中的运算符,就像根据参数类型找到函数一样
-
二元运算符@,x的类型是X,y的类型是Y,x@y的解析过程如下(一元运算符类似):
-
如果X是自定义类,查找X是否有成员operator@或者X的基类是否有operator@
-
在x@y的上下文中查找是否有operator@声明
-
如果X定义在namespace N中,在N的范围内查找operator@的声明
-
如果Y定义在namespace M中,在M的范围内查找operator@的声明
如果找到多个operator的声明,则用重载解析选择最佳匹配
-
-
待续
十九章 特殊运算符
- 待续
二十章 派生类
-
将一个类用作基类,等价于定义一个该类的对象,因此类必须定义后才能用作基类
-
默认情况下,覆盖虚函数的函数自身也变为virtual,派生类中可以重复关键字virtual,但非必须(建议不要重复)。使用override明确标记为覆盖版本。
-
使用作用域解析运算符::调用函数能保证不使用virtual机制。如果一个虚函数也是一个inline,对于使用::限定的调用就可以进行内联替换
-
使用覆盖控制:
-
virtual:函数可能被覆盖,用来引入虚函数
-
=0 : 函数必须是virtual,且必须被覆盖
-
override:指出函数要覆盖基类中的一个虚函数;
-
final: 函数不能被覆盖;
如果不使用覆盖控制,一个非static成员函数为虚函数,当且仅当它覆盖了基类中的一个virtual函数。
-
-
override是一个上下文关键字,应放在声明的最后,它不是函数类型的一部分,类外定义不能重复关键字
-
final是一个上下文关键字,如果在类名后加上final,可以将一个类的所有virtual成员函数声明为final,同时也阻止了从一个类进一步派生其他类。final说明符不是函数类型的一部分,不能在类外定义中重复。final的使用应反应语义需求
-
using 基类成员
- 函数重载不会跨越作用域
- 由using声明引入派生类作用域的名字,其访问权限由using声明所在位置决定
- 不能使用using指示将一个基类的所有成员都引入一个派生类
-
继承构造函数
//模版的写法
template
struct Vector : std::vector
{ using vector
:vector; }
//普通类的写法
struct B1{
B1(int){}
}
Struct D1:B1{
using B1::B1;//隐式声明D1(int)
string s;
int x;
}
void test(){
D1 d{6}; // d.x未初始化
D1 e; //错误,D1没有默认构造函数
}
D1::s被初始化而 D1::x未初始化的原因是继承的构造函数等价于只初始化基类的构造函数。一种解决办法是使用类内初始化器
通常仅对不增加数据成员的简单情形使用继承构造函数
-
返回类型放松
覆盖函数的类型必须与它所覆盖的虚函数的类型完全一致,C++对这一规则提供了一种放松规则:即如果原返回类型为B*,则覆盖函数的返回类型可以为D*,只要B是D的一个公有基类即可。类似的,返回类型B&可放松为D&
-
构造函数需要确切了解要创建的对象的类型,所以它不能是virtual的。构造函数不完全是一个普通函数,不能把它的指针传递给对象创建函数。
-
有纯虚函数的类表示抽象类,其不能创建对象,通常没有构造函数,但定义一个虚析构函数却很重要
-
如果纯虚函数在派生类中没有定义,它仍然是一个纯虚函数,派生类也是一个抽象类
-
接口继承、实现继承可以组合使用
-
访问控制
-
private: 仅可被所属类成员函数和友元函数访问
-
protected:仅可被所属类成员函数和友元函数以及派生类的成员函数和友元函数所访问
-
public:可被任何函数所使用
访问控制对名字的应用是一致的,不管名字引用的是什么,如数据、函数、类型、常量等
-
-
待续
二十一章 类层次
- 待续