再学 C++ 程序设计
这是一个大学程序设计课的复习笔记,这里默认大家学习过 C 语言程序设计。
1 from C to C++
- 1.1 C++ 的源代码后缀是
.cpp,编译器是 GNU C++,或简写为 g++。
C++ 是典型的面向对象编程语言(Object Oriented programming, OOP),当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
- 1.2 流输入输出
包含在 iostream 头文件中,输入的语法是 cin >> a; 输出为 cout << a;。C++ 作为经典的面向对象语言,学习之前需要知道什么是“对象”。
对象是现实世界或抽象世界中事物的一种计算机表示。可以理解为一个“变量”就是一个对象,一个函数也是对象等。对象是类的实例。类是对象的模板,定义了对象的属性和行为。
而 cout,cin,cerr 则是 iostream 头文件中的输入输出流对象。这里“流”大概是取自“水流”这个词。
<< 和 >> 分别被重载为输出和输入运算符。如果成功,会返回一个指向左值的引用,这允许链式调用。如果提取失败,比如因为输入的数据类型与变量的类型不匹配,状态会变为失败(fail),并且会返回一个错误的状态。
控制操作符:https://zhuanlan.zhihu.com/p/24926755931
控制小数输出:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double n = 123.4567;
cout << fixed << setprecision(10) << n;
}
举例:在 C++ 中,操纵符 endl 的作用是什么?
答:输出换行符 \n 并刷新输出流。
scientific:用于将输出设置成科学计数法。
fixed:用于定点,单独使用则保留小数点后六位。
例题:cout << 1 + "20.24" << endl << 20.24; 这一段代码,实际上第一段代码表示指针的移动。就是"20.24"这个字符串其实是其第一个元素的地址,而+1则是从第二位开始。
- 1.3 命名空间
C++ 的命名空间机制可以用来解决复杂项目中名字冲突的问题。
举个例子:C++ 标准库的所有内容均定义在 std 命名空间中,如果你定义了一个叫 cin 的变量,则可以通过 cin 来访问你定义的 cin 变量,通过 std::cin 访问标准库的 cin 对象,而不用担心产生冲突。
命名空间用下述代码声明和使用:
namespace xxx{
...
void f() {
std :: cout << "Hello World\n";
}
}
int main() {
xxx :: f();
}
命名空间也是可以嵌套的。还可以使用 using :: 指令:
using xxx::f 这条指令可以让我们省略某个成员名前的命名空间,直接通过成员名访问成员,相当于将这个成员导入了当前的作用域。
using namespace xxx 这条指令可以直接通过成员名访问命名空间中的任何成员,相当于将这个命名空间的所有成员导入了当前的作用域。
在所有文件中,所有同名命名空间块的内容会被合并到同一命名空间。
- 1.4 预处理命令
预处理命令就是预处理器所接受的命令,用于对代码进行初步的文本变换,比如 文件包含操作 #include 和处理宏 #define 等。
(挖坑)
- 1.5 引用
引用可以看成是 C++ 封装的非空指针,可以用来传递它所指向的对象,在声明时必须指向对象。
引用不是对象,因此不存在引用的数组、无法获取引用的指针,也不存在引用的引用。
引用可以看成对象的别名,在声明是不会额外生成新的对象。
通常我们会接触到的引用都为左值引用,即只绑定到左值的引用,同时只有 const 限定的左值引用可以绑定右值。
#include <iostream>
#include <string>
int main() {
std::string s = "Ex";
std::string& r1 = s;
const std::string& r2 = s;
r1 += "ample"; // 修改 r1,即修改了 s
// r2 += "!"; // 错误:不能通过到 const 的引用修改
std::cout << r2 << '\n'; // 打印 r2,访问了s,输出 "Example"
}
左值引用最常用的地方是函数参数,用于避免不需要的拷贝。函数的返回值也可也作为引用:
#include <iostream>
#include <string>
// 参数中的 s 是引用,在调用函数时不会发生拷贝
char& char_number(std::string& s, std::size_t n) {
s += s; // 's' 与 main() 的 'str'
// 是同一对象,此处还说明左值也是可以放在等号右侧的
return s.at(n); // string::at() 返回 char 的引用
}
int main() {
std::string str = "Test";
char_number(str, 1) = 'a'; // 函数返回是左值,可被赋值
std::cout << str << '\n'; // 此处输出 "TastTest"
}
https://oi-wiki.org/lang/reference/
- 1.6 string 类
C++ 提供了以下两种类型的字符串表示形式:C 风格字符串、C++ 的 string 类。
/* string 类的几种创建方式 */
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1;// 这样就创建了一个空的字符串
string s2 = "c++";
string s3 = s2;
string s4(5, 's');
string s5 = NULL;// 无法运行,这样的语法是错误的
}
和许多 STL 容器相同,string 能动态分配空间,这使得我们可以直接使用 std::cin 来输入,但其速度则同样较慢。这一点也同样让我们不必为内存而烦恼。
string 的加法运算符可以直接拼接两个字符串或一个字符串和一个字符。string 重载了比较运算符,同样是按字典序比较的,所以我们可以直接调用 std::sort 对若干字符串进行排序。
https://oi-wiki.org/lang/csl/string/
find(str,pos) 函数可以用来查找字符串中一个字符/字符串在 pos(含)之后第一次出现的位置(若不传参给 pos 则默认为 \(0\))。如果没有出现,则返回 string::npos(被定义为 \(-1\),但类型仍为 size_t/unsigned long)。
insert(pos,len,str) 代表在 pos 这个位置插入 len 个 str。
问:执行 clear() 后,capacity() 的值:
答:不变。
string 类型重载了 += 运算符,可以通过这个往字符串后面添加单个字符或者一个 string 类。
string 类包含 compare 函数,compare(a,b) 返回一个整数 \(-1,0,1\) 代表 \(a\) 的字典序小于、等于或大于 \(b\) 的字典序。
- 1.7 const,constexpr
const 修饰的变量被定义为了只读,不可以修改,const 修饰的指针和引用也是如此。关于指针还需注意:
int* const p1; // 指针常量,初始化后指向地址不可改,可更改指向的值
const int* p2; // 常量指针,解引用的值不可改,可指向其他 int 变量
const int* const p3; // 常量指针常量,值不可改,指向地址不可改
关于 const 成员函数:
const 限定的成员函数,可以用来限制对成员的修改。任何成员函数的参数都是含有 this 指针的,这个参数就是对象本身。如果一个对象被定义成了 const,则其 this 指针的类型也是 const,只能被 const 成员函数修改。
const 成员函数不能调用非 const 成员函数,const 成员函数不能修改成员变量,常量不能调用非 const 成员函数。
C++11 标准新添加的关键字 constexpr,声明编译时可以对函数或变量求值。即限定为常量表达式或限定为编译时可优化执行的函数。
- 1.8 auto 关键字
auto 关键字在编译时自动替换为对应的数据类型。
auto 的作用就是为了简化变量初始化,如果这个变量有初始化表达式,就可以用auto代替类型声明。也就是说 auto 声明的变量必须初始化。
- 1.9 new 和 delete
C 语言中用 malloc 和 free 申请动态空间,当在 C++ 中处理对象时很容易内存泄漏。C++ 提供了封装好的 new 和 delete 关键字。new 关键字直接返回对应类型的指针。
//申请空间
int* ptr = new int;
//申请空间并初始化
int* ptr2 = new int(1);
//申请连续的空间,空间大小为4*10=40
int* arr = new int[10];//(C++11)
//释放单个空间
delete ptr;
delete ptr2;
//释放连续的多个空间
delete[] arr;
malloc 和 free 不会对我们自定义类型完成初始化和资源的清理(也就是不会调用构造函数和析构函数),而 new 可以完成对象的初始化和 delete 可以完成对象的资源清理。这在学习了第 2 部分的内容会更好理解。
动态创建的堆空间对象不会自动析构,所以一定需要记得 delete。
栈内存和堆内存:栈内存由编译器自动管理(如函数调用时的局部变量),程序员不需要手动释放。堆内存需要程序员显示管理 (通过 new/delete),忘记释放会导致内存泄漏。栈内存不能随时增长和缩小!栈的大小是固定的(通常由编译器/操作系统预设,如 1-8MB),且无法动态调整。如果超过栈大小会导致栈溢出。堆内存才是灵活的:可以动态分配任意大小的内存(仅受系统内存限制),并支持动态调整。
坑点:对于基本类型数组,错误的使用 delete p(而非 delete[] p)是未定义行为,程序可能会崩溃。
2 类与面向对象编程
类(class)是结构体的拓展,不仅能够拥有成员元素,还拥有成员函数。
在面向对象编程(OOP)中,对象就是类的实例,也就是变量。
- 2.1 类的声明
类似 C 语言的 struct,C++ 中利用 class 关键字声明类。C++ 中同时保留了 struct 关键字,等同于 class 的 public。
/* 一个实例 */
class Object {
public:
int weight;
int value;
} e[array_length];
const Object a;
Object b, B[array_length];
Object *c;
关于访问说明符
public:该访问说明符之后的各个成员都可以被公开访问,简单来说就是无论类内还是类外都可以访问。
protected:该访问说明符之后的各个成员可以被类内、派生类或者友元的成员访问,但类外不能访问。
private:该访问说明符之后的各个成员只能被类内成员或者友元的成员访问,不能 被从类外或者派生类中访问。
一般情况下,数据成员需要是私有的,想要访问必须通过类内的函数进行访问。调用者不需要知道类的数据成员,而可以直接通过调用成员函数。同时也不需要知道成员函数的内部具体实现。
- 2.2 成员函数
成员函数可以定义在类定义内部,或者单独使用作用域解析运算符 :: 来定义。
举例:
// 这两份代码是等价的
class object{
private:
int a;
public:
void set(int _a) {
a = _a;
}
};
class object{
private:
int a;
public:
void set(int _a);
};
void object::set(int _a) {
a = _a;
}
成员函数的外部实现
在 DATE.cpp 中预处理引入 DATE.h,同时需要用到
::运算符来指明该函数是类 DATE 的成员函数。DATE.cpp中有时还包括DATE内部要使用到的函数,例如DaysInMonth。这种函数并非对外公开供用户使用,因此可以将其声明为类的私有成员。若在该函数中没有涉及该类的数据成员,则无需将它们声明为类的成员。
- 2.3 构造函数
类的构造函数是类的一种特殊的成员函数,每次创建类的新对象时执行它完成初始化等逻辑。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。
注意:如果用户没有自定义构造函数,则编译会自动生成一个默认构造函数。
// Example:
class Object {
public:
int weight;
int value;
Object() {
weight = 0;
value = 0;
}
};
/*
该例定义了 Object 的默认构造函数,
该函数能够在我们实例化 Object 类型变量时,
将所有的成员元素初始化为 0。
(C++11)一种等价的写法是:
Object() : weight(0), value(0) {}
关于这种写法的原理,以后会提及。一个注意的点是这个花括号是函数体。
*/
构造函数也可以带有参数。这样在创建对象时就可使用参数构造对象。注意:用户一旦定义了构造函数,编译器就不再自动添加默认构造函数,这时调用无参构造会出错。
如果仍然需要默认构造函数,用户可以再自行定义,或者使用 default 关键字。
在 C++ 中约定如果一个类中自定义了带参数的构造函数,那么编译器就不会再自动生成默认构造函数,也就是说该类将不能默认创建对象,只能携带参数进行创建一个对象;
但有时候需要创建一个默认的对象但是类中编译器又没有自动生成一个默认构造函数,那么为了让编译器生成这个默认构造函数就需要 default 这个属性。
#include <iostream>
using namespace std;
class A {
public:
A(int x) {
cout << "This is a parameterized constructor";
}
A() = default;
};
=default 仅要用于编译器能自动生成的特殊成员函数,包括默认构造函数,析构函数,赋值运算符,复制构造函数。构造函数不能是私有的,必须是公有的。
- 2.4 析构函数
每一个变量都将在作用范围结束走向销毁。一个类可以有任意多个构造函数,但只能有一个析构函数。
但对于已经指向了动态申请的内存的指针来说,该指针在销毁时不会自动释放所指向的内存,需要手动释放动态内存。
如果结构体的成员元素包含指针,同样会遇到这种问题。需要用到析构函数来手动释放动态内存。
析构函数(Destructor)将会在该变量被销毁时被调用。重载的方法形同构造函数,但需要在前加 ~
- 2.5 再谈 this 指针
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址(this 指针是指向当前对象的指针)。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
当成员函数参数与成员数据重名时,必须用 this 访问成员数据。
this 指针还可以用来实现链式调用。它通过在每个成员函数中返回 *this,使得多个成员函数调用可以在同一行内连续进行。
- 2.6 类的静态成员
静态(static)成员是类的组成部分但不是任何对象的组成部分。静态数据成员具有静态(全局)生存期,是类的所有对象共享的存储空间,是整个类的所有对象的属性,而不是某个对象的属性。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。
与非静态数据成员不同,静态数据成员不是通过构造函数进行初始化,而是必须在类定义体的外部再定义一次,且恰好一次,通常是在类的实现文件中再声明一次,而且此时不能再用 static 修饰。静态成员函数不能直接访问类的非静态成员,只能直接访问类的静态数据或函数成员。
- 2.7 弃置函数(=delete)
简单来说就是把某个函数删除,删除的函数定义必须是函数的首次声明。
- 2.8 成员初始化器列表
构造函数名字、参数列表和函数体之间,可选初始化器列表。初始化器列表以冒号开头,后跟一系列以逗号分隔的成员初始化器,如成员初始化构造函数、该类的其他构造函数。
必须使用【成员初始化列表】的情况
- const 类型成员
- 初始化成员是对象
- 继承类初始化基类的 private 成员
注意:数据成员被初始化的顺序与他们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
。
当默认成员初始化器和构造函数的初始化列表同时存在时,初始化列表的初始化优先级更高。
- 2.9 拷贝构造函数
用本类型对象引用作第一形式参数的构造函数。该参数传递方式为对象引用,避免在函数调用过程中生成形参副本。该形参声明为const,以确保在拷贝构造函数中不修改实参的值。
若用户未提供拷贝构造函数,则该类使用由系统提供的缺省拷贝构造函数。
(挖坑)
3 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名和参数列表。
重载的运算符可以理解为带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。
- 3.1 重载运算符的方式
一般有两种方式:
其一,类内重载(即重载为成员函数):
当重载为成员函数时,因为隐含一个指向当前成员的 this 指针作为参数,此时函数的参数个数与运算操作数相比少一个。
class Example {
// 成员函数的例子
return_type operator operator_name (除本身外的参数) { /* ... */ }
};
其二,类外重载:
return_type operator operator_name(所有参与运算的参数) { /* ... */ }
自增自减运算符
自增自减运算符分为两类,前置(++a)和后置(a++)。为了区分前后置运算符,重载后置运算时需要添加一个类型为 int 的空置形参。
struct MyInt {
int x;
// 前置,对应 ++a
MyInt &operator++() {
x++;
return *this;
}
// 后置,对应 a++
MyInt operator++(int) {
MyInt tmp;
tmp.x = x;
x++;
return tmp;
}
};
- 3.2 赋值运算符的重载
类会自动构建赋值运算符,而类内建的赋值运算符返回 *this,故用户自己定义的赋值运算符也应该返回 *this。
利用赋值运算符重载特性,可实现多种语义的赋值,常见的:
浅拷贝赋值
由缺省的赋值运算符完成,会按成员声明顺序逐一调用成员的赋值运算。当成员是指针时,被赋值对象与赋值对象共用一个资源
深拷贝赋值
由用户定义的的赋值运算符完成。该函数不需要逐一调用非指针成员的赋值运算(但建议按声明顺序逐一赋值)。
当成员是指针时,需要释放已拥有资源,复制赋值对象对应的资源,并指向该资源的副本,即被赋值对象拥有赋值对象资源的副本。
- 3.3 下标运算符
C++ 规定,下标运算符 [] 必须以成员函数的形式进行重载。
class IntArray {
private:
int *a;
int n;
public:
IntArray(int n=1):n(n) {…}
IntArray(const IntArray& other) {…}
~IntArray() {…}
int& operator[](int i) {
if (i>=0 && i < n) {
return a[i];
}
throw std::out_of_range("out of range");
}
const int& operator[](int i) const{
if (i>=0 && i < n) {
return a[i];
}
throw std::out_of_range("out of range");
}
};
在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应 const 对象,因为通过 const 对象只能调用 const 成员函数,如果不提供第二种形式,那么将无法访问 const 对象的任何元素。
- 3.4 函数调用运算符
函数调用运算符 () 只能重载为成员函数。通过对一个类重载 () 运算符,可以使该类的对象能像函数一样调用。
重载 () 运算符的一个常见应用是,将重载了 () 运算符的结构体作为自定义比较函数传入优先队列等 STL 容器中。
struct student {
string name;
int score;
};
struct cmp {
bool operator()(const student& a, const student& b) const {
return a.score < b.score || (a.score == b.score && a.name > b.name);
}
};
// 注意传入的模板参数为结构体名称而非实例
priority_queue<student, vector<student>, cmp> pq;
- 3.5 友元函数
运用类外定义,实现自己定义的类的读入、输出能重载是常见的事,但有时候类的成员是 private 的,外部函数无法访问,这又该如何解决?
可以使用友元函数,在之前加上 friend 关键字。类内部声明 friend 普通函数,函数虽然不属于类,但却可以访问类的变量及函数(包括私有的)。也可以在类外面声明友元函数。单函数都需要在类外实现。
其他类的成员函数也可以声明成友元函数,但有时一个一个声明太过麻烦,可以直接声明友元类。
4 继承和派生
在 C++ 中,当定义一个新的类 B 时,如果发现类 B 拥有某个已写好的类 A 的全部特点,此外还有类 A 没有的特点,那么就不必从头重写类 B,而是可以把类 A 作为一个“基类”(也称“父类”),把类 B 写为基类 A 的一个
“派生类”(也称“子类”)。这样,就可以说从类 A “派生”出了类 B,也可以说类 B “继承”了类 A。
派生类是通过对基类进行扩充和修改得到的。基类的所有成员自动成为派生类的成员。派生类对象的存储结构中,基类成员的存储位置在派生类新增成员之前。
- 4.1 继承的语法
class 派生类名 : 继承访问控制 基类类名{
成员访问控制:
成员声明列表;
}
继承访问控制和成员访问控制均由保留public、protected、private来定义,缺省均为private。
- private(私有的):
private后声明的成员称为私有成员,私有成员只能通过本类的成员函数来访问。不可被继承 - public (公有的):
在public后声明的成员称为公有成员,公有成员描述一个类与外部世界的接口,类的外部(程序的其它部分的代码)可以访问公有成员。可以被继承 - protected(受保护的):
受保护成员具有private与public的双重角色。类的外部不可以访问,如同 private;派生类可以访问,如同 public。可以被继承,派生类的成员函数可以访问。即:protected成员只能由本类及其直接/间接派生类的成员函数访问。
无论采用什么继承方式,基类的私有成员在派生类中都是不可访问的。“私有”和“不可访问”有区别:私有成员可以由派生类本身访问,不可访问成员即使是派生类本身也不能访问。大多数情况下均使用public继承。
使用 using 关键字可以改变基类成员在派生类的访问权限。只能改变基类中 public 和 protected 成员的访问权限。使用语法 using 基类名::基类名 可以使用基类构造函数。
- 4.2 基类初始化和构造函数
基类的构造函数不被继承,派生类种需要声明自己的构造函数。
派生类的构造函数种只需要对被本类种新增的成员进行初始化即可。而对于继承来的基类成员,是通过编译在派生类构造函数初始化器种自动使用无参构造函数(或拷贝构造函数)来完成的(如果没有则编译错误)。
如果派生类的构造函数需要使用基类的有参构造函数,必须显示地再初始化器列表种申明,注意不能在构造函数内调用。
在派生类的析构函数中不用显式调用基类的析构函数,因为每个类只有一个析构函数。
- 4.3 构造函数和析构函数调用顺序
构造函数先调用基类函数地构造函数(多重继承时按照声明从左向右的顺序调用),然后调用被雷对象成员的构造函数(也是按照声明顺序),最后调用本类得构造函数。
而析构的过程与上述过程恰恰相反,先调用本类的析构函数,再调用本类对象的析构函数,最后调用基类的析构函数。
-
4.4 类不可继承的成员
-
私有成员
-
构造函数和析构函数
-
4.5 派生与成员函数
派生类的成员函数包括以下几种类型:
重载:
- 具有相同的作用域(即同一个类定义中);
- 函数名字相同
- 参数类型(包括const 指针或引用) ,顺序 或 数目不同
覆盖:
是指派生类中存在重新定义的函数。其函数名,参数列表,都必须同基类中被重写的函数一致,返回值类型除了协变情况下也必须和基类中被重写的函数一致,只有函数体不同(花括号内)。派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。基类中被重写的函数必须有virtual修饰。
隐藏:
- 派生类的函数与基类的函数同名,但是参数列表有所差异。
- 派生类的函数与基类的函数同名,参数列表也相同,但是基类函数没有virtual关键字。
继承——没有被覆盖或者隐藏的基类函数,包括被重载的基类函数。
- 4.6 类型兼容性
赋值运算的类型兼容性:可以将后代类的对象赋值给祖先类的休想,反之不可。每个派生类对象包含一个基类部分,这意味着可以将派生类对象当作基类对象使用。
基类类型的指针或引用可指向/医用共有派生类型的对象,只有公有派生类才能兼容基类类型。
- 4.7 类型转化
1、向上类型转换(Upcasting)
向上类型转换是将派生类对象的指针或引用转换为基类类型的指针或引用。它是自动、安全的,不需要显式的类型转换操作。
2、向下类型转换(Downcasting)
向下类型转换是将基类对象的指针或引用转换为派生类类型的指针或引用。这种转换不自动进行,因为一个基类可以有多个派生类,需要进行运行时类型识别(RTTI)。这通常通过 dynamic_cast 实现。
C++ 新提供了四种显示转化类型函数:
1,静态转换:static_cast <要转换的类型>(变量名或表达式)
2,动态转换 dynamic_cast,主要用于上述用途。
用dynamic_cast对指针进行向下转型时,如果目标对象不是所期望的派生类类型,则结果会返回空指针。使用 dynamic_cast 进行转换时,若基类未声明虚函数,编译将失败
3,常量转换。移除或添加对象的 const 或 volatile 修饰符。
C++ 禁止将 const 指针隐式转换为非 const 指针(除非使用 const_cast)。顶层 const 规则:添加顶层 const 总是安全的(隐式允许),移除顶层 const 需要显式转换。同理,T 只能转换为 const T&,不能直接转换为 T&。
4,重新解释转换(reinterpret_cast) 它允许在不同类型之间进行低级别的类型转换。与其他类型转换操作符不同,reinterpret_cast 并不会进行任何类型检查,它仅仅是重新解释二进制位的含义,直接将一个类型的位模式重新解释为另一个类型。
- 4.8 多重继承和虚基类
派生类能同时从两个具有相同成员名的基类继承,这时只需要显示调用即可。访问时需用 作用域解析运算符 :: 消除二义性。出现同名函数时,优先调用派生类的函数。
继承多个基类,或者菱形继承,这时二义性的问题需要用虚继承解决。虚继承类时,在继承访问控制前添加保留字“virtual”。那么这个基类就是一个虚拟基类。普通基类与虚基类之间的唯一区别只有在派生类重复继承了某一基类时才表现出来。
创建后代类对象时,当该后代类列出的虚基类构造函数被调用,Virtual关键字保证了虚基类的唯一副本只被初始化一次。
创建派生类对象时构造函数的调用次序:
• 最先调用虚基类的构造函数;
• 其次调用普通基类的构造函数,多个基类则按派生类声明时列出的次序、从左到右调用,而不是初始化列
表中的次序;
• 再次调用对象成员的构造函数,按类声明中对象成员出现的次序调用,而不是初始化列表中的次序
• 最后执行派生类的构造函数。
虚基类不可以完全消除多重继承中的二义性问题
- 4.9 友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
friend class 类名;
需要注意的是,友元关系不可继承。
5 多态和虚函数
派生类对象使用它的基类的指针(或引用)调用虚函数时,将调用该对象的成员函数(也就是必须是向上转型)。
C++ 中,多态意味这允许对象表现出多种行为。通过将基类的函数定义为虚函数来实现。
虚函数通过在对象中添加指向虚函数表的指针来实现动态绑定。
在公有继承层次中,某个类的成员函数申明为virtual。则其直接或间接派生类中的相同签名的函数,都是虚函数。不论是否用关键字virtual再次申明。任何虚函数都有可能被其派生类在重新定义,重新定义后依然是虚函数。因此,虚函数既可以被继承,也可以被重新定义。
含有一个纯虚函数的类,叫做纯虚类。纯虚类不可以定义对象,通常是作为接口使用。接口只能包含纯虚函数。
纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求任何派生类都必须定义自己的版本。
override关键字的作用是确保派生类函数正确重写基类虚函数。
用 final 修饰字修饰的函数声明表明“该函数为最终覆盖,进制后续派生类再次覆盖”。
虚析构函数:
虽然析构函数是不继承的,但若基类声明其析构函数为 virtual,则派生的析构函数始终覆盖它。这使得指向基类的指针可以delete动态分配的类型。
普通纯虚函数不需要定义,但是纯虚析构函数必须定义。
6 模板和 STL
泛型编程:独立于任何特定数据类型的编程,这使得不同类型的数据(对象)可以被相同的代码操作————主要特点。
当从通用代码创建实例代码时,具体数据类型才被确定。泛型编程是一种编译时多态性(静态多态)。其中,数据类型本身是参数化的,因而程序具有多态性特征
实例化(Instantiation):由编译器将通用模板代码转换为具体类型不同的实例代码的过程称为实例化
- 5.1 函数模板
函数模板的一般形式:
template<模板形参表>
返回值类型 函数名 (形式参数列表)
{
函数体
}
以 swap 函数为例:
template <typename T>
void Swap( T& v1, T& v2)
{
T temp;
temp = v1;
v1 = v2;
v2 = temp;
}
模板的特化
当模板当中有些特殊的类型,当想要针对特殊类型进行一些特殊的操作,这时可以在正常的模板下面写一个空的 template<> 然后具体写这个函数的代码。
- 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
- 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步
- 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹配的函数则调用该函数。否则,进入第4步
- 提示编译错误
- 5.2 类模板
在类的声明前进行模板的声明。
template<模板形参表>
class 类模板名
{
类成员
}
类的函数在类外定义的时候和函数模板类似。
类模板的实例化
类模板是一个通用类模板,而不是具体类,不能用于创建对象,只有经过实例化后才得到具体类,才能用于创建对象。
实例化的一般形式:类模板名 <模板实参表>
*c++的模板编译模式
- 5.3 非类型模板形参
可以理解为模板内部的常量,类似普通的函数形参,对模板进行实例化时,非类型形参由相应模板实参的值代替。对应的,模板实参必须时编译时的常量表达式。
关于数组引用作为模板参数的函数定义:template<typename T, int N> void print(T (&arr)[N]);
6 STL
C++标准库的一部分,是帮你写好的部分,包括容器库、迭代器库和算法库
迭代器是用于遍历容器元素的指针对象的包装。重载了++,==等运算符。
decltype 说明符
decltype(expr)申明从表达式得到的类型
灵活使用 auto 和 decltype 可以减少程序对模板实参的依赖,提升程序通用性和可读性
- 6.1 容器
固定长度数组 array<int, n>
动态的连续数组 vector<int>
vector 的 reserve 函数:C++ 函数 std::vector::reserve() 保留向量容量的请求至少足以包含 n 个元素。如果需要更多空间,则会发生重新分配。
shrink_to_fit 函数:将 capacity 重新设置为 size。
双链表 list<int>
双端队列 deque<int>
7 异常处理
一个文件读写库函数在发现文件不存在时throw一个异常,调用该库的业务逻辑代码catch这个异常并提示用户“配置文件丢失”
在C++中,当执行throw语句时,通常会立即发生中断当前函数的正常执行流程,并开始寻找合适的catch块来处理异常
如果try块中没有抛出异常,那么其后的所有catch块都会被忽略
在catch块中,使用catch (const exception& e)相较于 catch (exception e)来捕获异常,其主要优势是:按引用捕获可以避免额外的对象拷贝开销,并防止“对象切片”问题。
在C++中,当一个函数内部抛出异常,但该函数自身没有提供相应的catch块时异常会沿着函数调用栈向上传播,直到找到一个匹配的catch块或者程序终止。
若一个类的构造函数在成功构造了其部分成员对象后,因某个操作(如动态内存分配)失败而抛出异常,已完全构造的成员对象的析构函数会被调用,但该类的析构函数不会。

浙公网安备 33010602011771号