C++从遗忘到入门
阿里妹导读
面向受众
面向对象:C++支持面向对象编程(OOP)的四大特性:封装、继承、多态和抽象。通过类和对象,程序员能够创建模块化的代码,更容易地进行维护和扩展。
泛型编程:C++支持模板编程,允许编写与数据类型无关的代码。模板是实现泛型编程的关键工具,它们提高了代码的复用性。
直接内存管理:C++提供了对内存的直接操作能力,允许程序员手动管理内存分配和释放,这是C++的一个强大特性,也是需要谨慎使用的地方,因为不当的内存管理可能会导致资源泄露和其他问题。
性能:C++编写的程序通常有很高的执行效率,这是因为C++提供了与底层硬件直接对话的能力。这使得C++成为开发要求性能的系统软件(如操作系统、游戏引擎)的理想选择。
C语言兼容:大部分C语言程序可以在C++编译器上编译并运行。这一特性简化了从C到C++的过渡。
多编程范式支持:除了面向对象和泛型编程外,C++还支持过程式编程和函数式编程等范式,使其成为一个多样化的工具,能适应不同的编程需求。
语法基础
类型
基础类型
int a; // 声明未初始化,使用前建议手动初始化char b = 'a';float c = 1.0f; // C++中默认小数是double类型,加上f可以指定为floatdouble d = 2.0;auto e = 20; // 编译器自动推导auto为 int 类型
基础类型隐式转换
安全的隐式转换:
整型提升:小的整型(如 char、short)会自动转换成较大的整型(如 int)。
算术转换:例如,当 int 和 double 混合运算时,int 会转换为 double。
存在隐患的隐式转换:
窄化转换:大的整数类型转换到小的整数类型,或者浮点数转换到整数,可能会造成数据丢失或截断。
指针转换:例如,将 void* 转换为具体类型的指针时,如果转换不正确,会导致未定义行为。
结构体(struct)
// 结构体定义struct Person {std::string name;int age;}// 结构体初始化,Person person = {"Jim", 20};Person person2; // 创建另一个实例person2 = person;// 将person中的值复制到person2中,默认是浅拷贝,在有指针的情况下有潜在风险
枚举(enum)
enum Color { RED, GREEN, BLUE };// 使用Color myColor = RED;
enum class Color {RED,GREEN,BLUE};Color myColor = Color::RED; // 使用作用域解析运算符(::)访问枚举值
联合体(union)
// 联合体的定义union Data {int intValue;float floatValue;char charValue;}// 联合体一次只能保存一种类型的数据,每次赋值都会覆盖内存中之前的值// 因此联合体一般是配合结构体来使用,下面是一个示例// 定义数据类型的枚举enum DataType {INT,FLOAT,CHAR};// 定义一个结构体,它包含一个联合体和一个枚举标签struct SafeUnion {// 标记当前联合体中存储的数据类型DataType type;// 定义联合体union {int intValue;float floatValue;char charValue;} data;};// 赋值操作SafeUnion su;su.type = FLOAT;su.data.floatValue = 1.0f;// 使用时,通过type判断类型然后访问联合体对应的成员变量switch(su.type) {case FLOAT:cout << su.data.floatValue << endl;break;}
类(class)
class Person {public:void doWork(); // 方法,类对外提供的一系列操作实例的函数private:std::string name; // 成员变量,封装到类中的属性,保存内部状态信息int age;};
列表初始化
// 基础类型int a{0};double b{3.14};// 结构体struct MyStruct {int x;double y;};MyStruct s{1, 2.0};// 类class MyClass {public:MyClass(int a, double b) : a_(a), b_(b) {}private:int a_;double b_;};MyClass obj{5, 3.14}; // MyClass 必须有一个匹配这个参数列表的构造函数// 数组int arr[3]{1, 2, 3};// 上面介绍的都是现代C++推荐写法,省略 =// 下面的2种写法绝大多数情况下是等价的float arr[2]{1, 2}; // 写法1float arr[2] = {1, 2}; // 写法2// 编译器对这两种写法的处理是一致的,方法2并不会产生临时变量和拷贝赋值,包括类的声明
int a = 7.7; // 编译能通过,但是有warningint b = {1.0}; // 编译器拒绝通过,因为浮点到整形的转换会丢失精度
零初始化
数组
数组的声明
Typename arrayName[Size];// 基本类型int arr[10];char charArr[30];// 复杂类型struct Point {int x;int y;}Point points[10];
数组的初始化
int arr[] = {1, 2, 3, 4, 5}; // 数组大小为5,编译器自动确定int arr[10] = {1, 2, 3}; // 数组前三项确定为1,2,3,其余被初始化为0int arr[10] = {0}; // 整个数组全部为0
数组的使用
int arr[10] = {}; // 零初始化arr[0] = 10; // 修改数组第一个元素值为10
数组在声明后(无论静态声明还是动态声明),数组的大小即固定,不可更改;
数组不提供任何内置的方法来获取其大小,通常需要额外保存数组的大小,或者使用特殊标记结束元素(C风格的字符串使用'\0'表示数组结束);
数组不提供边界检查,越界访问的代码是可以通过编译的(静态数组编译器会给出警告),可能导致很多潜在问题。
int arr[10] = {0};int a = arr[10]; // 最大的有效索引是9,这里出现越界,但编译器能顺利编译通过(有警告)// a中的值是不确定的,没有实际意义的,这里是读取,危害可能有限arr[10] = 99;// 可怕的是该语句也能通过编译,但这里进行了更加危险的操作,// 越界访问了一块内存并修改了其内容,这很可能导致程序崩溃
多维数组
二维数组
Typename arrayName[Rows][Columns]; // Rows是行数, Columns是列数, 必须常量// 实际示例int arr[10][10]; // 定义了一个10*10的二维数组
// 完全初始化int matrix[2][3] = {{1, 2, 3},{4, 5, 6}};// 部分初始化int matrix[2][3] = {{1, 2}, // 第一行的最后一个元素将被初始化为 0{4} // 第二行的第二个和第三个元素将被初始化为 0};// 单行初始化int matrix[2][3] = {1, 2, 3}; // 只初始化第一行,其他行将默认初始化为0// 自动推断,和一维数组一样,编译器会根据数组推断二维数组第一维的大小int matrix[][3] = {{1, 2, 3},{4, 5, 6}};
多维数组
Typename arrayName[Depth][Rows][Columns];数组的替代
std::vector: 可变大小的数组。提供对元素的快速随机访问,并能高效地在尾部添加和删除元素。
std::list 双向链表。支持在任何位置快速插入和删除元素,但不支持快速随机访问。
std::deque: 双端队列。类似于std::vector,但提供在头部和尾部快速添加和删除元素的能力。
std::array (C++11): 固定大小的数组。提供对元素的快速随机访问,并且其大小在编译时确定。
std::forward_list (C++11): 单向链表。提供在任何位置快速插入和删除元素,但不支持快速随机访问。
std::stack: 栈容器适配器。提供后进先出(LIFO)的数据结构。
std::queue: 队列容器适配器。提供先进先出(FIFO)的数据结构。
std::priority_queue: 优先队列容器适配器。元素按优先级出队,通常使用堆数据结构实现。
std::set: 一个包含排序唯一元素的集合。基于红黑树实现。
std::multiset: 一个包含排序元素的集合,元素可以重复。基于红黑树实现。
std::unordered_set (C++11): 一个包含唯一元素的集合,但不排序。基于散列函数实现。
std::unordered_multiset (C++11): 一个包含元素的集合,元素可以重复,但不排序。基于散列函数实现。
指针
理解指针
a是一个整形,占用4个字节(一般int类型占用4字节),0xffffffffffffecdc是其首地址,内存中的值是2568,即代码中的赋值(具体的存储细节可以搜索 大端序、小端序)
p是一个整形指针,占用8个字节(64位系统),0xffffffffffffece0是其首地址,内存中的值是a变量内存的首地址,即0xffffffffffffecdc。
指针的定义
Typename * ptrName;// 指针定义风格,下面的声明都正确int *p; // C风格,旨在强调 (*p)是一个整形值int* p; // 经典C++风格,只在强调 p是一个整形指针类型(int*)// 集团推荐的风格,指针、引用都是居中,两边留空格int * p; // 指针int & a = xx; // 左值引用int && a = xx; // 右值引用
int a = 123; // 假设 a 地址为 0xfffff100int * p = &a; // 此时 p 中存储的值为 0xfffff100p = p + 1; // 此时 p 中存储的值为 0xfffff104 (0xfffff100偏移4个字节,即int变量占用的大小)
指针初始化和访问
int a = 5;int * p = &a; // & 取地址运算符// * 用在指针这里是解引用运算符,可以获取指针指向的地址的值cout << *p << endl; // 输出 5int b = 10;p = &b; // 指针变量可以修改其指向地址cout << *p << endl; // 输出 10
常量指针 vs 指针常量
常量指针
int value = 5;const int * p = &value; // p是一个常量指针int const * q = &value; // 和上面的声明等价*p = 10; // 非法,*p是常量不能修改int a = 6;p = &a; // 合法,p本身不是常量,可以重新赋值
指针常量
int value = 5;int * const p = &value; // p是常量*p = 6; // 合法int a = 7;p = &a; // 非法
指针与数组
指针和数组名的异同
int arr[5] = {1, 2, 3, 4, 5};int * p1 = arr; // arr 被当做指向数组首元素的指针int * p2 = &arr[0]; // 取arr首个元素的地址// 这种情况下 p1 和 p2 是等价的if (p1 == P2) { // 检测会通过cout << "p1,p2是等价的" << endl;cout << *p1 << endl; // 打印 1cout << *p2 << endl; // 打印 1}// 使用指针访问数组// 指针方式cout << *(p1 + 1) << endl; // 访问数组第二个元素,这种方式符合指针的计算规则// 类似数组名的使用方式cout << p1[1] << endl;// p1虽然是指针,索引访问方式依然有效,本质是*(p1 + 1)的语法糖
int arr[5] = {1, 2, 3, 4, 5};int * p1 = arr;cout << sizeof(arr) << endl; // 打印结果:20cout << sizeof(p1) << endl; // 打印结果:8// sizeof(arr)为数组本身的大小,这里是 5个int占用20字节// sizeof(p1)为指针本身大小,64位系统中占用8个字节
cout << &arr << endl; // 0x16b98aa40cout << &arr + 1 << endl; // 0x16b98aa54cout << &arr[0] << endl; // 0x16b98aa40cout << &arr[0] + 1 << endl; // 0x16b98aa44// 可以看出 &arr 和 &arr[0] 的值是一样的,但是指针偏移1后// (&arr + 1) 在 &arr 的基础上偏移了20(0x14)个字节// (&arr[0] + 1) 在 &arr[0] 的基础上偏移了4个字节
&arr 得到的类型是 int (*)[5] ,这是一个指向包含5个整数数组的指针 &arr[0]得到的类型是 int *,这是一个整型指针
动态数组
int * arr = new int[10]; // new操作符在堆内存中申请10个int类型大小的连续空间,并返回首地址arr[0] = 1;arr[1] = 2;// ...delete[] arr; // new操作符申请的内存需要使用delete操作符释放,数组使用delete[]
// 二维数组的动态创建 & 释放int rows = 5; // 行数int cols = 3; // 列数// 动态创建二维数组int ** array = new int*[rows]; // 创建行指针for (int i = 0; i < rows; ++i) {array[i] = new int[cols]; // 为每行分配内存}// 初始化二维数组for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {array[i][j] = i * cols + j; // 或者任何其他的赋值逻辑}}// 使用二维数组,例如打印它for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {std::cout << array[i][j] << ' ';}std::cout << std::endl;}// 动态释放二维数组for (int i = 0; i < rows; ++i) {delete[] array[i]; // 释放每行的内存}delete[] array; // 释放行指针数组的内存
int * p[10]; // p是一个包含10个int变量的数组int (*p)[10]; // p是一个指向拥有10个int变量的数组的指针// * [] 两个运算符的优先级不同,[]的优先级更高// 第一个语句声明了 p[10], int * 是类型// 第二个语句有括号改变了优先级,因此 p 是一个指针,剩下的部分定义了类型// 下面函数指针也会有类似的定义int (*pf)(int, int); // pf是指向形如 int func(int, int) 的函数指针
函数
函数基础
返回类型:函数可能返回的值的数据类型。如果函数不返回任何值,则使用关键字 void。
函数名:用于识别函数的唯一名称。
参数列表:括号内的变量列表,用于从调用者那里接收输入值。如果函数不接受任何参数,则参数列表为空。
函数体:花括号 {} 内包含的代码块,当函数被调用时将执行这些代码。
// 这是一个简单函数定义int add(int a, int b) {return a + b;}// 调用int sum = add(3, 7); // sum值为10
// 下面是一个函数原型的定义int draw(int, int); // 函数原型的参数列表可以省略参数名int draw(int width, int height);// 建议加上参数名,可以更直观的了解参数含义
参数传递
值传递 指针传递 引用传递(特指左值) 右值传递
传递方式
值传递
void swap(int a, int b) {int tmp = a;a = b;b = tmp;}int x = 5;int y = 7;swap(x, y); // x = 5 y = 7
指针传递
void swap(int * a, int * b) {int tmp = *a;*a = *b;*b = tmp;}int x = 5;int y = 7;swap(&x, &y); // x = 7 y = 5
int sum(int arr[], int size);// 定义1,这种定义的好处是清晰,调用者一看就知道传递数组指针int sum(int * arr, int size);// 定义2,这种定义更符合数组传参的本质// 特殊说明// 不管定义1还是定义2,通过参数传递数组指针后,数组指针(前面介绍过,即数组名)会退化为首个元素// 的地址指针,因此一定要通过size参数传递数组的大小给函数
引用传递(左值传递)
int a = 5;int & ra = a; // ra的类型是 int&(引用),必须声明时立即初始化int b = 6;ra = b; // 非法,引用变量不支持重新赋值
void swap(int & a, int & b) {int tmp = a;a = b;b = tmp;}int x = 5;int y = 7;swap(x, y); // x = 7 y = 5
右值传递
左值(lvalue): 左值是指表达式结束后依然存在的持久性对象,可以出现在赋值语句的左边。 左值可以被取地址,即可以通过取地址运算符&获取其地址。 通常,变量、数组元素、引用、返回左值引用的函数等都是左值。 右值(rvalue): 右值是指表达式结束后不再存在的临时对象,不能出现在赋值语句的左边。 右值不能被取地址,即不能通过取地址运算符&获取其地址。 通常,字面量、临时对象、返回右值引用的函数等都是右值。
纯右值 将亡值 泛左值 std::move 类型萃取
参数修改保护
// 下面指针传递示例void printInfo(int arr[], int size); // 内部可修改arrvoid printInfo(const int arr[], int size); // 内部不可修改arr// 下面是引用传递示例void printInfo(std::string& info); // 内部可以修改infovoid printInfo(const std::string& info); // 内部不可以修改info
传参方式选择原则
如果数据对象很小,如内置数据类型或者小型结构,这按值传递。
如果数据对象是数组,这使用指针,因为这是唯一的选择,并将指针声明为常量指针。
如果数据对象较大的结构,则使用常量指针或者const引用,可以节省复制结构所需要的时间和空间,提高程序的效率。
如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用。这是C++增加引用的主要原因。因此传递类对参数的标准方式是按引用传递。
如果数据对象是内置数据类型,则使用指针。 如果数据对象是数组,则只能使用指针。 如果数据对象是结构,则使用指针或者引用。 如果数据对象是类对象,则使用引用。
函数重载
// 下面是一组重载函数,同样是计算两个数的和,针对不同类型提供了不同的定义int add(int a, int b) { // 版本1return a + b;}float add(float a, float b) { // 版本2return a + b;}double add(double a, double b) { // 版本3return a + b;}add(1, 2); // 匹配版本1add(1.0f, 2.0f); // 匹配版本2add(1.0, 2.0); // 匹配版本3add(1.0f, 2.0) // 匹配 ??? (匹配版本3,原因可以搜索 ”重载解析“)
重载规则
函数名称 参数列表:包括参数的类型、数量和顺序。注意一个参数是否使用引用并不能作为签名不同的依据。参数是否是 const 能作为不同的依据
函数签名必须不同 作用域必须相同:重载的函数必须处于同一个作用域,否则它们被视为不同作用域中的不相关函数。 最佳实践是尽量保持重载函数的明确性,避免产生容易混淆的重载集合。
// 下面两个版本的函数不算重载,因为两者调用时的表达式都是 add(x, y), 编译器无法区分int add(int a, int b);int add(int & a, int & b);// 下面两个版本算重载,编译器会根据实参是否是常量来匹配更合适的版本int add(const int a, const int b);int add(int a, int b);
重载函数匹配规则
函数模板
函数模板的声明
template <typename T>T add(T a, T b) {return a + b;}// 多类型的定义template <typename T1, typename T2>void funcName(T1 a, T2 b);
#include <iostream>using namespace std;// 函数原型template <typename T>T add(T a, T b);int main() {cout << add(1, 2) << endl; // 3cout << add(1.0f, 2.1f) << endl; // 3.1cout << add(1.0, 3.2) << endl; // 4.2return 0;}template <typename T>T add(T a, T b) {return a + b;}
重载的模板
// 函数原型template <typename T>T add(T a, T b);template <class T> // 声明模板时 typename 和 class 等价T add(T a, T b, T c);// 函数定义略
模板的局限
template <typename T1, typename T2>void funcName(T1 x, T2 y) {...?type? temp = x + y;...}
template <typename T1, typename T2>void funcName(T1 x, T2 y) {...decltype(x + y) temp = x + y;...}
template <typename T1, typename T2>?type? funcName(T1 x, T2 y) {...return x + y;}
// 正常函数声明int add(int a, int b);// 返回类型后置声明auto add(int a, int b) -> int;// 利用该语法可以这么声明上面的函数(推荐C+11中使用)template <typename T1, typename T2>auto funcName(T1 x, T2 y) -> decltype(x + y) {...return x + y;}// C++14及以后得标准拓展了auto的类型推导能力auto funcName(T1 x, T2 y) {...return x + y;}
模板函数具体化、全特化 (Full Specialization) auto类型推导规则(区分C+11、C++14) decltype类型推导规则 模板元编程
回调函数
函数指针
// 返回类型 (*指针变量名)(参数列表);// 示例int add(int a, int b) {return a + b;}int (*pf)(int, int) = add; // 可以这么理解定义:因为(*pf)表示函数,那么pf就是函数的指针// 类似数组,函数指针也有两种使用方式cout << (*pf)(2, 3) << endl; // 5 指针使用方式cout << pf(2, 3) << endl; // 5 直接作为函数名使用// 函数指针的定义一般都不怎么直接,使用也不方面// 经典C++中可以使用typedef简化这个定义typedef int (*p_fun)(int, int); // 现在p_fun就是一种类型名称p_fun pAdd = add; // 精简很多// 现代C++提供了 using 语法让这个过程更加直观,推荐使用using p_fun = int (*)(int, int); // 可读性更强p_fun pAdd = add;// auto大杀器auto pAdd = add; // 懒人利器
#include <iostream>void callBack(int costTimeMs);void work(void (*pf)(int));int main() {work(callBack);}void callBack(int costTimeMs) {using namespace std;cout << "costTime:" << costTimeMs << endl;}void work(void (*pf)(int)) {std::cout << "do some work" << std::endl;// ...pf(123); // (*pf)(123) 也ok}
std::function
#include <functional>#include <iostream>using namespace std;void callBack(int costTimeMs) {cout << "costTime:" << costTimeMs << endl;}void work(function<void(int)> callBack) {callBack(1234);}int main() {function<void(int)> func = callBack;work(func);return 0;}
更多方式
类(class)
定义类
class MyClass {public:// 公共成员,通常的对外提供的方法定义void setMember(int member);private:// 私有成员,成员变量,仅供内部调用函数int mMember; // 集团规范推荐,使用m前缀void innerFunc(); // 函数一律小驼峰protected:// 受保护成员,成员变量,供子类调用函数};
// person.hpp// C++一般使用 hpp 后缀的头文件,表明包含C++特性的代码(模板、引用、类等)// .h .hpp只是约定的做法,不是语法上的必要性// 类的定义一般放到头文件中,用来对外声明类// 类的头文件规范:类名小写 + '_'分割(如果有多个单词的case)// 如果类名:PersonInfo 对应头文件:person_info.hpp#ifndef PERSON_H#define PERSON_H#include <string>#include <iostream>// 声明 'Person' 类class Person {public:// 构造函数声明Person(const std::string & name, int age);// 成员函数声明void printInfo() const; // const成员函数,保证函数不会修改调用对象// Setters 和 Getters 声明void setName(const std::string & name);const std::string & getName() const;void setAge(int age);int getAge() const;private:// 成员变量std::string mName;int mAge;};#endif // PERSON_H// person.cpp// 实现代码放到同名cpp文件中#include "person.hpp"// 构造函数定义Person::Person(const std::string & name, int age) : mName(name), mAge(age) {}// 成员函数定义void Person::printInfo() const {std::cout << "Name: " << mName << ", Age: " << mAge << std::endl;}// Setters 和 Getters 定义void Person::setName(const std::string & name) {mName = name;}const std::string & Person::getName() const {return mName;}void Person::setAge(int age) {mAge = age;}int Person::getAge() const {return mAge;}
const成员函数
访问控制
Public(公共):公共成员可以在类的外部被访问。 Private(私有):私有成员只能在类的内部被访问。 Protected(受保护):受保护成员可以在类的内部以及其派生类中被访问。
构造函数
class MyClass {public:MyClass(int m1, int m2, int m3) : mM1(m1), mM2(m2), mM3(m3) {}private:int mM1;const int mM2;int & mM3;};// 类的初始化方式MyClass a1(1, 2, 3); // 传统构造函数MyClass a1 = MyClass(1, 2, 3); // 同上MyClass a2 = {1, 2, 3}; // 列表初始化,会匹配最合适的构造函数MyClass a3{1, 2, 3}; // 同上
explicit 关键字
析构函数
class MyClass {public:MyClass() {// 构造函数分配资源或执行初始化data = new int[10]; // 假设动态分配了内存}~MyClass() {// 析构函数释放资源delete[] data; // 释放动态分配的内存}private:int* data; // 指向动态分配的内存};
运算符重载
class MyClass {public:MyClass() : data(new int[10]) { } // 构造函数~MyClass() { delete[] data; } // 析构函数// 拷贝赋值运算符MyClass & operator=(const MyClass& other) {if (this != &other) { // 避免自赋值std::copy(other.data, other.data + 10, data);}return *this;}private:int* data;};// 使用MyClass a;MyClass b = a; // 默认的赋值操作是浅拷贝,这里因为重载了 = 运算符,变成深拷贝// C++11开始可以删除默认的赋值操作符,从而防止因浅拷贝带来的风险class MyClass2 {// ...MyClass2 & operator=(const MyClass2 & other) = delete; // 禁用赋值操作符// ...};MyClass2 a;MyClass2 b = a; // 非法,MyClass2的 = 运算符被禁用
C++支持重载的运算符 转换函数(这个不算运算符重载,例:operator int())
拷贝构造函数和拷贝赋值运算符
#include <iostream>class MyClass {public:MyClass() : data(new int[10]) { } // 默认构造函数~MyClass() { delete[] data; } // 析构函数// 拷贝构造函数MyClass(const MyClass & other) : data(new int[10]) {std::copy(other.data, other.data + 10, data);std::cout << "copy init" << std::endl;}// 拷贝赋值运算符MyClass & operator=(const MyClass & other) {if (this != &other) { // 避免自赋值std::copy(other.data, other.data + 10, data);}std::cout << "copy =" << std::endl;return *this;}private:int* data;};int main() {MyClass a;MyClass b;MyClass c = a;c = b;return 0;}// 程序输出// copy init// copy =
浅拷贝 深拷贝
移动构造函数和移动赋值运算符(C++11)
#include <iostream>using namespace std;class BigMemoryPool {private:static const int POOL_SIZE = 4096;int* mPool;public:BigMemoryPool() : mPool(new int[POOL_SIZE]{0}) {cout << "call default init" << endl;}// 编译器会优化移动构造函数,正常情况可能不会被执行// 可以添加编译选项 “-fno-elide-constructors” 关闭优化来观察效果BigMemoryPool(BigMemoryPool && other) noexcept {mPool = other.mPool;other.mPool = nullptr;cout << "call move init" << endl;}BigMemoryPool & operator=(BigMemoryPool && other) noexcept {if (this != &other) {this->mPool = other.mPool;other.mPool = nullptr;}cout << "call op move" << endl;return *this;}void showPoolAddr() {cout << "pool addr:" << &(mPool[0]) << endl;}~BigMemoryPool() {cout << "call destructor" << endl;}};BigMemoryPool makeBigMemoryPool() {BigMemoryPool x; // 调用默认构造函数x.showPoolAddr();return x; // 返回临时变量,属于右值}int main() {BigMemoryPool a(makeBigMemoryPool());a.showPoolAddr();a = makeBigMemoryPool();a.showPoolAddr();return 0;}// 输出内容call default initpool addr:0x152009600instance addr:0x16fdfeda0pool addr:0x152009600instance addr:0x16fdfeda0 // 编译器优化,这里a和x其实是同一个实例,因此不会触发移动构造call default initpool addr:0x15200e600 // 新的临时变量,堆内存重新分配instance addr:0x16fdfed88 // 临时变量对象地址call op move // 移动赋值call destructorpool addr:0x15200e600 // a的Pool指向的内存地址变成新临时对象分配的地址,完成转移instance addr:0x16fdfeda0 // a对象的地址没有变化call destructor
返回值优化(RVO) 命名返回值优化(NRVO)
友元函数和友元类
友元函数
#include <iostream>// 声明 Vector2D 类class Vector2D {private:float x_;float y_;public:Vector2D(float x = 0.0f, float y = 0.0f) : x_(x), y_(y) {}// 友元函数声明,用于重载 + 操作符friend Vector2D operator+(const Vector2D & a, const Vector2D & b);// 输出 Vector2D 对象的友元函数friend std::ostream & operator<<(std::ostream & out, const Vector2D & v);};// 重载 + 操作符的友元函数定义Vector2D operator+(const Vector2D & a, const Vector2D & b) {return Vector2D(a.x_ + b.x_, a.y_ + b.y_);}// 重载 << 操作符的友元函数定义,用于输出 Vector2D 对象std::ostream & operator<<(std::ostream & out, const Vector2D & v) {out << "(" << v.x_ << ", " << v.y_ << ")";return out;}int main() {Vector2D vec1(1.0, 2.0);Vector2D vec2(3.0, 4.0);Vector2D vec3;vec3 = vec1 + vec2; // 使用友元函数重载的 + 操作符std::cout << "vec1: " << vec1 << std::endl;std::cout << "vec2: " << vec2 << std::endl;std::cout << "vec3: " << vec3 << std::endl; // 输出: vec3: (4, 6)return 0;}
友元类
#include <iostream>class MyClass; // 前向声明// 声明一个类(FriendClass),该类将访问MyClass的私有和受保护成员class FriendClass {public:void accessMyClass(MyClass & obj);};// 声明主类(MyClass)class MyClass {private:int secret;public:MyClass(int val) : secret(val) {}// 声明FriendClass为MyClass的友元类friend class FriendClass;};// FriendClass成员函数实现void FriendClass::accessMyClass(MyClass & obj) {// 可以访问MyClass的私有成员'secret'std::cout << "MyClass secret value is: " << obj.secret << std::endl;}int main() {MyClass obj(42); // 创建MyClass对象FriendClass friendObj; // 创建FriendClass对象friendObj.accessMyClass(obj); // 访问MyClass的私有成员return 0;}
继承
class Base {// 基类成员};class Derived : public Base {// 派生类成员};
公有继承(public)最常见的继承类型。在公有继承中,基类的公有成员和保护成员在派生类中保持其原有的访问级别,而基类的私有成员在派生类中是不可访问的。
保护继承(protected)基类的公有成员和保护成员都成为派生类的保护成员。这意味着它们只能被派生类或其进一步的派生类中的成员函数访问。
私有继承(private)私有继承会将基类的公有成员和保护成员都变成派生类的私有成员。这意味着这些成员只能被派生类的成员函数访问,而不能被派生类的派生类访问。
虚继承
多态
#include <iostream>class Base {public:void baseMethod() {std::cout << "Base method" << std::endl;}virtual void polymorphicMethod() {std::cout << "Base polymorphic method" << std::endl;}virtual ~Base() {} // 虚析构函数,用于多态};// 公有继承派生类class Derived : public Base {public:// 重写基类的虚函数void polymorphicMethod() override {Base::polymorphicMethod(); // 可以通过添加限定域调用基类实现std::cout << "Derived polymorphic method" << std::endl;}};int main() {Derived d;d.baseMethod(); // 调用基类的方法d.polymorphicMethod(); // 调用派生类重写的方法Base *b = &d;b->polymorphicMethod(); // 通过基类指针调用派生类的方法,体现多态return 0;}
虚函数表 动态绑定
抽象类和纯虚函数
// Interface in C++class IShape {public:virtual void draw() const = 0; // 纯虚函数virtual ~IShape() {} // 虚析构函数以确保派生类的析构函数被调用};class Circle : public IShape {public:void draw() const override {// 实现绘制圆形的代码}};class Rectangle : public IShape {public:void draw() const override {// 实现绘制矩形的代码}};
模板类
template <typename T>class MyTemplateClass {const T& getValue();public:T myValue;};
容器类
智能指针
// 简单示例// 定义智能指针// C++11语法std::unique_ptr<MyClass> my_unique_ptr(new MyClass());std::shared_ptr<MyClass> my_shared_ptr(new MyClass());// C++14提供了更安全更现代的方法auto my_unique_ptr = std::make_unique<MyClass>();auto my_shared_ptr = std::make_shared<MyClass>(); // 可以按照构造函数的定义传参// 调用类的方法和普通指针类似my_unique_ptr->func();my_shared_ptr->func();// 在需要传对象指针和引用的场景// 类指针类型void testFunc1(MyClass * p);testFunc1(my_unique_ptr.get()); // 通过get获取原始指针// 引用类型void testFunc2(MyClass & ref);testFunc2(*my_unique_ptr); // 通过*运算符获取对象的引用
函数对象
// 封装函数void printHello() {std::cout << "Hello, World!" << std::endl;}std::function<void()> func = printHello;// 封装Lambda表达式std::function<int(int, int)> add = [](int a, int b) -> int {return a + b;};int sum = add(2, 3); // sum 的值为 5// 封装成员函数class MyClass {public:void memberFunction() const {std::cout << "Member function called." << std::endl;}};MyClass obj;std::function<void(const MyClass &)> f = &MyClass::memberFunction;f(obj); // 输出: Member function called.// 封装带有绑定参数的函数void printSum(int a, int b) {std::cout << "Sum: " << a + b << std::endl;}int main() {using namespace std::placeholders; // 对于 _1, _2, _3...// 绑定第二个参数为 10,并将第一个参数留作后面指定std::function<void(int)> func = std::bind(printSum, _1, 10);func(5); // 输出: Sum: 15return 0;}