基础知识
- 1. 指针、引用
- 2. 数组
- 3. 缺省参数
- 4. 函数重载
- 5. 内联函数
- 6. 宏
- 7. auto
- 8. const
- 9. 类和对象
- 10. 类的6个默认成员函数
- 11. 初始化列表
- 12. this指针
- 13. C/C++的区别
- 14. C++ 三大特性
- 15. 结构体内存对齐规则
- 16. explicit
- 17. static
- 18. 友元类、友元函数
- 19. 内部类
- 20. 内存管理(虚拟地址空间)
- 21. 堆上开辟空间(malloc、calloc、realloc、free)
- 22. new、delete操作符
- 23. 内存泄漏
- 24. 智能指针
- 25. 四种转换
- 26. 继承
- 27. 多态
- 28. 模版类、模版函数
- 29. 深、浅拷贝
- 30. 二叉搜索树
- 31. 红黑树
- 32. AVL树
- 33. 哈希
- 34. STL
- 35. C++ 11
- 36. lambda 表达式
- 37. 进程与线程
- 38. C++ 线程库
- 39. B/B+树
- 40. 异常
- 41. 程序运行四个阶段
1. 指针、引用
指针: 一个实体,存放指向对象地址的变量。指向一块内存,指向可以改变,有const和非const的区别,甚至可以为空(NULL)。
引用: 变量(内存)的别名,一经定义不可修改,且必须初始化。
int A=10;
int& rA = A; //引用
int* pA = &A; // 指针
char* p = "hello"; //字符指针
int* arr[10]; //指针数组
int (*p)[10]; //数组指针
int *p = &A; //整形指针
float f = 1.0; float* pf = &f; //单精度浮点型指针
double d = 2.00; double* pd = &d; //双精度浮点型指针
注:指针与对应的变量类型保持一致。若类型不匹配可能存在以下问题:
- 错误的数据解释。解引用出错。
- 内存访问错误。触发违例,访问错误。
- 类型安全问题。破坏类型安全性,难以调试和理解。
野指针(wild): 没有经过初始化的指针。
悬空指针(dangling): 指针指向已经被释放的的内存空间的指针。
规避野指针:
- 指针创建立即初始化。
- 使用指针过程中,防止指针越界访问。
- 指针指向的空间释放,指针立即置为NULL。
- 使用指针前进行安全检查。
🔥 指针和引用的区别:
指针 | 引用 | |
---|---|---|
NULL(nullptr) | 存在空(NULL)指针 | 无空引用 |
初始化 | 定义可不初始化,使用再进行初始化 | 定义必须初始化 |
指向 | 指向可以改变 | 初始化完毕,指向不可更改,不可引用其他实体 |
多级 | 存在多级指针(如:二级指针等) | 无多级引用 |
访问 | 需要显式解引用,才能获取值(如:*p) | 编译器处理,无须显式解引用 |
参数 | 实质是传值,传递的值是指针的地址 | 实质是传地址,传递的是变量的地址 |
sizeof | 32位操作系统:4Byte 64位操作系统:8Byte | 实体类型的大小(如:int 4Byte) |
自增+1 | 指针向后偏移一个类型大小 | 实体自增+1 |
安全性 | 存在野指针,安全性比引用差 | 安全性好 |
2. 数组
定义:数组是一种数据结构,用于存储固定大小的同类型元素的集合。每个元素可以通过索引访问,索引从0开始。
特点:
- 固定大小:数组的大小在声明时指定,并且在运行时不能更改。
- 内存连续性:数组中的元素在内存中是连续存储的。
- 索引访问:通过索引可以快速访问数组中的任何元素(索引访问:O(1))。
注:
(1)越界访问:访问数组的非法索引会导致未定义行为,C++不会自动检查数组边界。
(2)数组与指针:在C++中,数组名通常被当作指针来处理,指向数组的第一个元素。
3. 缺省参数
缺省参数: 声明或者定义函数时,函数的参数有默认值。
// C语言不支持,C++支持
void func1(int x=1, int y=2){} //全缺省
void func2(int x, int y=2){} //半缺省
int main()
{
func1();
func2(1);
}
规则:
- 缺省参数必须是从右往左,连续给值,不能间隔。
- 缺省参数不能同时出现在声明和定义中。
- 缺省值是常量 /全局变量。
4. 函数重载
定义:同一作用域内,函数名称相同,参数列表(参数类型,参数个数,顺序)不同,构成函数重载。
原理:由于C++ 底层的重命名机制,将函数根据参数的个数,类型,返回值类型做了重命名。
C++底层重命名机制(Name Mangling):
为了支持函数重载,C++编译器采用了一种称为“名称重整”(Name Mangling)的技术。名称重整是指在编译过程中,编译器将每个函数的名称和参数类型编码为一个唯一的标识符,这样在生成目标代码时,即使是同名的函数,也会有不同的符号名,以避免冲突。
5. 内联函数
定义:inline修饰的函数,编译时代码展开,提升程序运行的效率(以空间换时间)。
适用性:不适合长代码,递归,循环。(不建议声明和定义分开,会导致链接错误)。
6. 宏
优点 | 缺点 |
---|---|
1. 增强代码复用性 2. 提高性能 | 1. 不方便调试(预编译宏替换) 2. 导致代码可读性差,可维护性差,容易误用 3. 没有类型安全检查 |
其他技术替换宏:
(1)常量定义使用const。
(2)函数定义使用内联函数。
7. auto
auto: auto 关键字是 C++11 引入的一项功能,它用于自动推导变量的类型。处理 STL 容器和迭代器时特别有用,因为它可以简化代码并减少冗长的类型声明。
类型推导规则:
(1)单一变量:根据初始化表达式的类型来推导变量的类型。
(2)多个变量:所有变量的类型都将根据第一个变量的初始化表达式进行推导。
注:
(1)如果初始化值是 const
或引用,auto 会推导成相应的 const 或引用类型。
(2)auto
默认情况下推导出的类型是值类型,如果需要引用类型,可以使用 auto&
或 const auto&
。
8. const
const: 在C++中,const关键字用于定义常量,表示值不可修改。它可以用于变量、指针、函数参数和类成员变量,成员函数等不同场景。
- 使用
const
关键字定义的变量在初始化后不能被修改。 - 指针 和指针指向的值都可以使用
const
关键字。
指向常量的指针: 指针本身可以修改,但不能通过指针修改它指向的值。
常量指针: 指针本身是常量,不能修改指向的地址,但可以通过指针修改指向的值。const int* ptr = &x; // 指向常量的指针 // *ptr = 20; // 错误:不能修改指向的值 int y = 30; ptr = &y; // 合法:可以改变指针指向
指向常量的常量指针: 指针本身和它指向的值都不能修改。int z = 40; int* const ptr2 = &z; // 常量指针 *ptr2 = 50; // 合法:可以修改指向的值 // ptr2 = &y; // 错误:不能修改指针指向
const int* const ptr3 = &x; // 指向常量的常量指针 // *ptr3 = 60; // 错误:不能修改指向的值 // ptr3 = &y; // 错误:不能修改指针指向
const
成员函数表示不会修改对象的状态,即成员变量的值。const
可以用于函数参数和返回类型,以确保在函数内部不修改传入的参数。- 类中使用
const
定义的成员变量必须在初始化列表中初始化。
9. 类和对象
类: 类是一个用户定义的数据类型,它描述了对象的属性和行为。类定义了对象的结构和方法,是对象的蓝图。类描述了一组有相同特性(属性)和相同行为的对象。
对象: 对象是类的实例化,是实际存在的实体。通过对象可以访问类的属性和方法。
在C++中,类成员可以有不同的访问权限。
访问控制:
public
: 公有成员,类外部可以访问。private
: 私有成员,只有类的内部可以访问。protected
: 受保护成员,只有类的内部和子类可以访问。
注:class
默认是private
,struct
默认是public
。
10. 类的6个默认成员函数
-
默认构造函数(Default Constructor):不带参数的构造函数。
说明:默认构造函数是在没有参数的情况下创建对象时调用的。如果类没有定义任何构造函数,编译器会自动生成一个默认构造函数。
构造函数的作用:初始化对象,当对象创建时调用构造函数。class MyClass { public: MyClass() {} // 默认构造函数 }; int main() { MyClass obj; // 调用默认构造函数 return 0; }
-
析构函数(Destructor):用于在对象生命周期结束时清理资源。
说明:析构函数用于在对象生命周期结束时执行清理操作。它的名称前有一个波浪号(~),并且没有参数和返回值。调用阶段:
- 当对象生命周期结束时调用析构函数。
- 对于栈上的对象(局部变量),当离开作用域时调用析构函数。
- 对于堆上的对象,当使用delete运算符时调用析构函数。
- 对于全局对象和静态对象,在程序结束时调用析构函数。
class MyClass { public: ~MyClass() {}// 析构函数 }; int main() { MyClass obj; // 对象生命周期结束时调用析构函数 return 0; }
-
拷贝构造函数(Copy Constructor):用于通过另一个同类型对象初始化新对象。
说明:拷贝构造函数用于通过另一个同类型的对象初始化新对象。它的参数是一个对同类型对象的常量引用。class MyClass { public: MyClass(const MyClass& other) {} // 拷贝构造函数 }; int main() { MyClass obj1; MyClass obj2 = obj1; // 调用拷贝构造函数 return 0; }
-
赋值运算符重载(Copy Assignment Operator):用于将一个对象赋值给另一个同类型对象。
说明:赋值运算符用于将一个对象赋值给另一个同类型的对象。它返回对当前对象的引用。class MyClass { public: MyClass& operator=(const MyClass& other) { if (this != &other) { // 拷贝赋值逻辑 } return *this; } }; int main() { MyClass obj1; MyClass obj2; obj2 = obj1; // 调用拷贝赋值运算符 return 0; }
-
移动构造函数(Move Constructor):用于通过另一个同类型的右值对象(临时对象)初始化新对象。
说明:移动构造函数用于通过另一个同类型的右值对象(临时对象)初始化新对象。它的参数是一个对同类型对象的右值引用。class MyClass { public: MyClass(MyClass&& other) noexcept { // 移动构造函数 } }; int main() { MyClass obj1; MyClass obj2 = std::move(obj1); // 调用移动构造函数 return 0; }
-
移动赋值运算符(Move Assignment Operator):用于将一个同类型的右值对象(临时对象)赋值给另一个对象。
说明:移动赋值运算符用于将一个同类型的右值对象(临时对象)赋值给另一个对象。它返回对当前对象的引用。class MyClass { public: MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { // 移动赋值逻辑 } return *this; } }; int main() { MyClass obj1; MyClass obj2; obj2 = std::move(obj1); // 调用移动赋值运算符 return 0; }
11. 初始化列表
初始化列表:在C++中,初始化列表是一种用于在构造函数中初始化类成员的语法。
class A{
public:
A(int b, int c)
:_b(b), _c(c)
{}
private:
int _b;
int _c;
};
优点:
- 提高效率:通过初始化列表,成员变量在对象创建时直接初始化,而不是先调用默认构造函数然后再赋值,这样可以避免不必要的赋值操作,提高效率。
- 支持常量成员和引用成员的初始化:常量成员和引用成员必须在初始化列表中初始化,因为它们在创建后不能被赋值。
- 支持无默认构造函数的成员初始化:如果成员变量的类型没有默认构造函数,它们必须在初始化列表中显式初始化。
注:初始化顺序,按照声明顺序,而不是初始化列表中的书写顺序。
12. this指针
this 指针:C++中的一个特殊指针,它指向调用成员函数的对象本身。每个成员函数都有一个隐含的参数 this,这个参数是一个指向当前对象的指针。通过 this 指针,成员函数可以访问调用它的对象的成员变量和其他成员函数。
特性:
- 在成员函数内部使用。
- 本质是成员函数的一个形参。(对象调用函数时,将地址作为实参传递给this形参,对象内部不存储this 指针)
- this 指针是成员函数第一个隐含的指标形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。
- static 成员不含this指针。
用途:
- 访问成员变量和成员函数:this 指针可以用于在成员函数内部访问对象的成员变量和成员函数。
- 返回对象本身:this 指针可以用于在成员函数中返回当前对象的引用或指针,以支持链式调用。
- 区分成员变量和参数:在成员函数的参数名与成员变量名相同时,可以使用 this 指针区分它们。
13. C/C++的区别
- C语言是面向过程语言,注重通过函数解决问题,C++是面向对象语言,注重模块化结构化,可维护性高。
- C++ 关键字增多,是对C语言的扩展。
- 源文件后缀名不同:C语言后缀为(.c),C++后缀为(.cpp)。
- 返回值类型不同:C语言默认为int整型,返回一个随机数(0xcccccccc),C++返回值默认为void。
- 参数列表,C++必须与声明的参数类型,个数保持一致,C语言无限制。
- C++支持函数重载,C语言不支持。
- C++支持指针和引用,C语言支持指针(传值/传址)。
- C++ 增加命名空间,作用域(A::a),输入输出和C语言也不同。
14. C++ 三大特性
C++的三大特性,封装,继承,多态。
封装:是 将数据(成员变量)和操作数据的函数(成员函数)包装在一个类中,从而实现对数据的保护和操作的统一。封装提供了数据隐藏和接口暴露两个重要功能,确保对象的内部状态只能通过公开的接口进行操作,从而提高了代码的安全性和可维护性。
继承:是从一个已有的类(基类或父类)创建一个新类(派生类或子类)的机制。 派生类继承了基类的所有非私有成员(属性和方法),并可以扩展或重写这些成员。继承允许代码的重用和类的扩展。
多态::是指 同一操作或方法调用在不同对象上可以表现出不同的行为。多态可以通过虚函数和继承实现。主要有两种多态:
- 静态多态(编译时多态):通过函数重载和运算符重载实现。
- 动态多态(运行时多态):通过虚函数和继承实现。
🔥 总结:
- 封装:通过隐藏类的内部实现细节,只暴露公共接口,提高了数据安全性和代码的可维护性。
- 继承:允许从已有的类创建新类,支持代码重用和类的扩展。
- 多态:允许通过相同的接口调用不同的实现,支持运行时动态绑定和扩展功能。
15. 结构体内存对齐规则
结构体的内存对齐规则旨在提高内存访问效率。内存对齐涉及如何将结构体的成员变量排列在内存中的问题,以满足特定的对齐要求。
内存对齐的基本概念:
- 对齐(Alignment):每个数据类型都有一个对齐要求,即它们在内存中必须按特定的字节边界对齐。例如,4字节对齐意味着数据的地址必须是4的倍数。
- 填充(Padding):为了满足对齐要求,编译器在结构体成员之间和末尾可能会插入一些未使用的字节,这些字节称为填充字节。
对齐规则:
- 每个成员的偏移量必须是其对齐大小的倍数。对齐大小通常是成员大小,但有时也可以是编译器指定的对齐值。
- 结构体的总大小必须是其最大对齐成员的倍数。为了确保数组中的每个结构体实例都正确对齐,结构体的总大小也会被填充到其最大对齐成员的倍数。
struct Example { char a; // 1字节 int b; // 4字节 short c; // 2字节 float d; // 4字节 double e; // 8字节 }; //24
优缺点:
优点 | 缺点 |
---|---|
1. 提高内存访问效率,尤其在硬件对齐要求严格的体系结构上。 2. 减少缓存未命中(cache miss)的概率,提高缓存利用率。 | 1. 增加内存消耗,填充字节占用额外的内存空间。 |
16. explicit
在C++中,explicit
关键字用于修饰构造函数,目的是 防止编译器在不经意间进行隐式类型转换 。默认情况下,C++允许通过构造函数进行隐式类型转换,这可能导致一些潜在的错误或不明确的行为。
作用:
- 防止隐式转换:explicit关键字告诉编译器,构造函数不应该被用作隐式转换的手段,只能通过显式调用来使用。
- 增强代码可读性:使用explicit可以使代码更加清晰,避免在类型转换时产生意外的结果。
17. static
在C语言和C++中,static关键字都有多个作用,但在C++中的应用更广泛。
相同与差异:
-
静态局部变量:定义在函数内部的局部变量,使用static关键字修饰后,这个变量在函数调用之间保持其值不变。
-
静态全局变量:定义在文件内部的全局变量,使用static关键字修饰后,这个变量的作用域仅限于定义它的文件。
-
静态函数:定义在文件内部的函数,使用static关键字修饰后,这个函数的作用域仅限于定义它的文件。
-
类的静态成员变量:属于整个类,而不是某个特定的对象。所有对象共享同一个静态成员变量。
特性:- 静态成员在类外定义,初始化。
- 静态成员没有this指针。
- 静态成员为所有类对象所共享,不属于某个具体实例。
-
类静态成员函数:类的静态成员函数不依赖于具体对象,可以通过类名直接调用。
C | C++ |
---|---|
1. 静态局部变量 2. 静态全局变量 3. 静态函数 | 1. 静态局部变量 2. 静态全局变量 3. 静态函数 4. 类的静态成员变量 5. 类的静态成员函数 |
总结:C++中做了扩展,增加了定义类的静态成员变量和静态成员函数。
18. 友元类、友元函数
C++中,友元函数(Friend Function)和友元类(Friend Class)是用于访问类的私有和保护成员的机制。它们提供了一种方式来允许特定的函数或类访问其他类的私有和保护成员。
友元类:是一个被特定类声明为友好的类,这样友元类的所有成员函数都可以访问被声明为友好的类的私有和保护成员。
友元函数:是一个被特定类声明为友好的函数,声明时需要加friend关键字。它可以访问该类的所有私有(private)和保护(protected)成员。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 友元函数不是该类的成员函数,但它们可以访问该类的内部数据。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用和原理相同。
友元关系的特点:
- 友元关系是单向的,不具有交换性,不能传递。(A是B的友元函数,A同时是C的友元函数,但是B和C不是友元关系)。
- 避免滥用友元函数和友元类,在一定程度上破坏了类的封装特性。
19. 内部类
定义:可以说两个类符合嵌套关系时,内部类是外部类的友元。
特性:
- 内部类可以直接访问外部类的static,enum(不需要类名)。
- sizeof(外部类)只是外部类大小,和内部类无关。
20. 内存管理(虚拟地址空间)
- 栈(Stack): 用于存放函数调用时的局部变量、返回地址、参数等。每调用一个函数,就会在栈上分配一块内存(称为栈帧),当函数返回时,这块内存会被释放。
特点:栈是自动管理的,当函数调用完成,栈帧会自动销毁。栈的内存空间有限,如果函数调用层次太深(例如递归调用过多),可能会发生栈溢出。 - 内存映射段(Memory-Mapped Segment): 允许程序将文件或设备的内容映射到内存中。程序可以直接访问这些内容而不必调用读写函数,从而提高了I/O操作的效率。
特点:处理大文件,共享内存,进程间通信等场景。 - 堆(Heap): 用于动态内存分配,程序可以在运行时使用new和delete(或malloc和free)在堆上分配和释放内存。
特点:堆的大小并不固定,可以根据程序的需要动态增长或收缩,但必须手动管理,防止内存泄漏。 - BSS段(Block Started by Symbol): 存放未初始化的全局变量和静态变量。在程序启动时,这些变量会被初始化为零。
特点:与数据段类似,但这些变量初始值为零。 - 数据段(Data Segment):存放已初始化的全局变量和静态变量。这些变量在程序的整个生命周期中都存在。
特点:在程序启动时,这部分内存已经分配,并在程序结束时释放。 - 代码段(Code Segment):存放程序的机器指令,即存放编译后的代码。代码段是只读的,不会在程序运行时发生变化。
特点:在程序执行期间,这部分内存通常是只读的,防止程序意外修改指令。
21. 堆上开辟空间(malloc、calloc、realloc、free)
malloc:返回开辟内存大小,需要强转,不强转返回(void*),不初始化,开辟成功返回空间首地址,开辟失败返回NULL。
void* malloc(size_t size);
calloc:同malloc,需要强转,返回(void*),参数不同,给定num,size返回空间大小(num * size )Byte,返回前会初始化空间为0 byte。
void* calloc (size_t num, size_t size);
realloc:重新对空间分配内存大小,若连续空间不够,则重新申请一块新的空间,返回新的内存的地址,否则在原来空间上追加,无合适的空间返回NULL。
void* realloc(void* memblock, size_t size);
free:释放空间,联合使用。
void free(void* memblock);
22. new、delete操作符
作用:申请自定义类型的空间,new会调用构造函数,delete会调用析构函数。底层也是使用malloc/free实现。
特性:
- new/delete 申请/释放单个空间,new[], delete[] 申请/释放连续空间。
- new 申请失败会抛异常。
malloc/free与new/delete的区别:
共同点 | 不同点 |
---|---|
1. 都是从堆上申请空间,需要手动释放。 | 1. malloc/free 是函数,new/delete 是操作符。 2. malloc 申请的空间不会初始化,new 会初始化。 3. malloc 申请空间需要计算空间大小并传递,new 只需要跟上相应类型即可。 4. malloc 申请返回值为(void*),需要强转,申请失败须判空(NULL),new 不需要强转,只需要捕获异常即可。 5. malloc/free 只开辟空间,new/delete 会调用构造函数和析构函数,完成对象的初始化和资源清理。 |
23. 内存泄漏
内存泄漏:导致程序占用越来越多的内存,最终可能导致系统资源耗尽。
🔥堆内存泄漏:
- 动态内存分配后未释放:使用malloc没有free,或者new没有delete,导致空间一直被占用。
void memoryLeakExample() { int* ptr = new int[10]; // 分配了内存 // 没有对应的 delete[],导致内存泄漏 }
- 早期返回导致未释放内存:由于条件分支或异常,导致没有到达释放内存的代码。
void earlyReturnExample(bool condition) { int* ptr = new int[10]; if (condition) { return; // 在返回之前未释放 ptr } delete[] ptr; }
- 未释放的对象或资源:类中分配的内存或资源未在析构函数中释放。
class Example { public: Example() { ptr = new int[10]; } ~Example() { /* 忘记了 delete[] ptr */ } private: int* ptr; };
- 循环引用:使用智能指针(如 std::shared_ptr)时,如果存在循环引用,会导致对象无法正确释放。
struct Node { std::shared_ptr<Node> next; }; void circularReferenceExample() { auto node1 = std::make_shared<Node>(); auto node2 = std::make_shared<Node>(); node1->next = node2; node2->next = node1; // 循环引用 }
🔥 处理方式:
- 手动检查:确保每一个 new 或 malloc 都有对应的 delete 或 free。
- 使用 RAII(Resource Acquisition Is Initialization)模式来确保资源在不再需要时自动释放。
- 使用智能指针:使用 std::unique_ptr 或 std::shared_ptr 来自动管理动态内存,避免手动释放的麻烦。
系统资源泄漏:比如套接字,文件描述符,管道没有使用对应的函数释放,造成资源浪费。
解决方式:使用检测工具,检测内存泄漏。
- Linux:使用Valgrind工具,可以在程序运行时检测内存泄漏 。
valgrind --leak-check=full ./your_program
- Windows:VLD 工具。
24. 智能指针
🔥 智能指针:C++ 标准库中的一种工具,用来自动管理动态内存,避免手动管理内存时容易出现的内存泄漏和指针悬挂等问题。
-
std::auto_ptr
(弃用):是C++98 引入的一种智能指针,用于管理动态分配的内存,以避免内存泄漏问题。然而,std::auto_ptr 存在一些严重的缺陷,导致它在 C++11 中被废弃,并在 C++17 中被完全移除。(以下简称:auto_ptr
)特点:
- 独占所有权:auto_ptr 采用独占所有权模型,一个 auto_ptr 对象只能拥有它所指向的资源,无法进行资源共享。
- 所有权转移:auto_ptr 支持赋值操作,但在赋值过程中,所有权会被转移:
- 当一个
auto_ptr
赋值给另一个auto_ptr
时,源指针会丧失对资源的所有权,并变为空指针。这种行为可能导致意外的资源转移和潜在的悬挂指针。 - 不安全的复制语义:因为 auto_ptr 在复制时会转移所有权,这意味着
auto_ptr
不支持普通的复制语义,这种行为很容易导致编程错误,特别是在函数参数传递时。
- 当一个
缺陷:
-
不安全的赋值行为:当
auto_ptr
进行赋值操作时,所有权转移的方式是隐式的,容易导致难以预料的行为。 -
无法与标准容器兼容:因为
auto_ptr
的复制语义,无法安全地将它存储在标准容器(如 std::vector、std::map)中。#include <memory> #include <iostream> void autoPtrExample() { std::auto_ptr<int> ptr1(new int(10)); std::auto_ptr<int> ptr2 = ptr1; // ptr1 失去所有权 std::cout << "ptr1: " << (ptr1.get() ? "Not null" : "Null") << "\n"; // 输出 "Null" std::cout << "ptr2: " << *ptr2 << "\n"; // 输出 "10" } //ptr1 将失去对所指对象的所有权,ptr2 接管所有权。这种所有权转移的行为是不安全的,可能会导致误用。
-
std::unique_ptr
(替换auto_ptr):C++11 引入了std::unique_ptr
,是一种独占所有权的智能指针,它保证一个对象只能由一个 std::unique_ptr 所拥有。对象的生命周期由std::unique_ptr
自动管理,当 std::unique_ptr被销毁时,所指向的对象也会被自动销毁。(以下简称:unique_ptr
)
特点:- 如果你需要独占所有权的智能指针,unique_ptr 是 auto_ptr 的现代、安全替代品。
- unique_ptr 明确要求通过 std::move 来转移所有权,这避免了auto_ptr 那样的隐式错误。
使用方式:
#include <memory> #include <iostream> class MyClass { public: MyClass() { std::cout << "MyClass constructed\n"; } ~MyClass() { std::cout << "MyClass destructed\n"; } }; void uniquePtrExample() { std::unique_ptr<MyClass> ptr1(new MyClass()); // 或者使用 std::make_unique(C++14 引入) auto ptr2 = std::make_unique<MyClass>(); // std::unique_ptr 不允许复制 // std::unique_ptr<MyClass> ptr3 = ptr1; // 错误 // 可以通过 std::move 转移所有权 std::unique_ptr<MyClass> ptr3 = std::move(ptr1); }
-
std::shared_ptr
: 是一种共享所有权的智能指针,多个 std::shared_ptr 可以共同管理同一个对象。当最后一个 std::shared_ptr 被销毁时,所指向的对象才会被释放。使用方式:
#include <memory> #include <iostream> class MyClass { public: MyClass() { std::cout << "MyClass constructed\n"; } ~MyClass() { std::cout << "MyClass destructed\n"; } }; void sharedPtrExample() { std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权 std::cout << "Reference count: " << ptr1.use_count() << "\n"; // 输出 2 }
-
std::weak_ptr
:是一种不拥有对象所有权的(弱引用计数)智能指针,它用于解决 std::shared_ptr 的循环引用问题。std::weak_ptr 不会影响对象的引用计数,因此即使所有 std::shared_ptr 都被销毁,对象也会被正确释放。使用方式:
#include <memory> #include <iostream> class MyClass { public: std::shared_ptr<MyClass> ptr; // 循环引用 MyClass() { std::cout << "MyClass constructed\n"; } ~MyClass() { std::cout << "MyClass destructed\n"; } }; void weakPtrExample() { std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); std::weak_ptr<MyClass> weakPtr = ptr1; // 不会增加引用计数 if (auto sharedPtr = weakPtr.lock()) { // 需要转换为 shared_ptr 才能使用 std::cout << "Object is still alive\n"; } else { std::cout << "Object has been destroyed\n"; } }
作用:解决 shared_ptr 的循环引用问题。
🔥 总结:
- 使用 std::unique_ptr 管理独占资源。
- 使用 std::shared_ptr 管理共享资源。
- 使用 std::weak_ptr 解决
std::shared_ptr
的循环引用问题。
25. 四种转换
C++11 中的四种类型转换方式包括: static_cast、dynamic_cast、const_cast 和 reinterpret_cast。这些转换方式提供了类型安全或特定目的的转换机制。
-
static_cast
:用于在 类型之间进行标准转换,例如基本数据类型的转换、父类和子类之间的转换等。
适用场景:用于没有运行时类型检查的转换,例如数值类型间的转换,或者类层次结构中已知类型的安全转换。int a = 10; double b = static_cast<double>(a); // int 转换为 double
-
dynamic_cast
:主要用于带有 多态的类层次结构之间的转换,能够在运行时进行类型检查,确保安全转换。
适用场景:用于将基类指针或引用转换为派生类指针或引用,并且该基类需要有至少一个虚函数。class Base { virtual void foo() {} }; class Derived : public Base {}; Base* base = new Derived(); Derived* derived = dynamic_cast<Derived*>(base); // 安全转换
-
const_cast
:用于在 类型上添加或移除 const 或 volatile 限定符。
适用场景:用于需要修改常量对象的情况下,但需确保没有违反 const 限定的语义。const int a = 10; int* p = const_cast<int*>(&a); // 移除 const 限定 *p = 20;
-
reinterpret_cast:用于对位模式进行重新解释的转换,它 可以将指针类型转换为不同类型的指针,也可以转换为整数类型。
适用场景:在需要直接修改对象的位模式或类型完全不相关的指针转换时使用,但需小心,因为可能导致未定义行为。int a = 42; void* p = &a; int* pInt = reinterpret_cast<int*>(p); // 强制转换回 int* 类型
26. 继承
继承(Inheritance): 是面向对象编程的一个重要概念,允许一个类(派生类)从另一个类(基类)获取属性和行为。 继承使得代码复用成为可能,还能增强代码的扩展性和灵活性。体现了面向对象程序设计的层次结构。
- 基本概念
基类(Base Class):也称父类或超类,是被继承的类。它包含一些派生类可以复用的成员变量和方法。
派生类(Derived Class):也称子类,是继承了基类的类。派生类可以复用基类中的数据成员和成员函数,还可以添加自己的成员。 - 继承类型
基类的private成员在派生类中,无论以什么方式继承,都是不可见的。(不可见:指基类的私有成员还是会被继承到派生类对象中,但是语法上限制派生类的访问,无论是类内还是类外)class Base { public: int a; protected: int b; private: int c; }; class Derived : public Base { // a 为 public,b 为 protected,c 不可访问 };
-
继承方式的可见性:public > protected > private。
-
class的默认继承方式为 private,struct的默认继承方式是 public。
-
继承时,要显示写出继承方式,public最多,其他方式较少,可维护性差。
-
赋值时,通过切片,派生类可以给基类的引用,指针对象进行赋值。
-
直接强转赋值(定义一个基类指针,强转为子类指针。会出现越界访问的情况)
-
继承的作用域:均拥有独立的作用域。
-
隐藏、重定义:基类和派生类内拥有同名成员,派生类将屏蔽基类成员,直接访问自己的成员。
-
派生类的默认成员:
- 构造函数:构造顺序是先基类,再子类。
- 拷贝构造
- 析构函数:析构顺序是先子类,再基类。
- 赋值运算符重载
- 取地址运算符重载
-
不能被继承的类
// C98 构造函数私有化. class A{ static A init(){ return A(); } private: A(){} }; //final关键字. class B final {};
- 友元关系不能被继承,友元关不能访问子类的保护和私有成员。
- static(静态成员),一个继承体系里面只有一个。
- 类外初始化
- .cpp与.h分开时,在cpp内部初始化,否则会报错(Link 2001),不能在main内定义,否则会报编译器错误(Error C2665)
- const static 一经定义不能修改
- 单继承,多继承,菱形继承
- 单继承:一个子类只有一个父类。
- 多继承:一个子类具有多个直接父类。
- 菱形继承:多继承的一种特例。具有数据冗余,二义性问题,可使用虚拟继承来解决。
class A { /* 基类 */ }; class B : virtual public A { /* 虚继承 */ }; class C : virtual public A { /* 虚继承 */ }; class D : public B, public C { /* 菱形结构,A 的成员只有一份 */ };
原理: 使用了多态,让类D在寻找A中的成员变量时,通过虚表指针 + 偏移量来获取地址/值。
27. 多态
多态(Polymorphism): 是面向对象编程的重要特性,它允许不同类的对象通过统一的接口表现出不同的行为。多态是实现类间共享接口的基础,使得程序能够通过基类指针或引用操作派生类对象。
静态多态: 是指在编译时确定调用哪个函数,通常使用函数重载或模板来实现 。静态多态不涉及运行时的动态分派,因此效率较高。
-
函数重载:通过相同函数名,但参数不同(数量、类型或顺序不同)来实现多态。
class Printer { public: void print(int i) { std::cout << "Printing integer: " << i << std::endl; } void print(double d) { std::cout << "Printing double: " << d << std::endl;} }; Printer p; p.print(42); // 调用 print(int) p.print(3.14); // 调用 print(double)
-
模板:通过模板参数的不同类型来实现多态。
template <typename T> void print(T value) { std::cout << "Printing value: " << value << std::endl; } print(42); // print(int) print(3.14); // print(double) print("Hello"); // print(const char*)
动态多态: 使用 virtual
关键字修饰基类中的函数,派生类通过重写(override)这些虚函数来提供具体实现。
-
多态实现
#include <iostream> using namespace std; class Base { public: virtual void speak() { // 声明虚函数 cout << "Base speaking" << endl; } virtual ~Base() {} // 虚析构函数 }; class Derived : public Base { public: void speak() override { // 重写基类的虚函数 cout << "Derived speaking" << endl; } }; int main() { Base* b = new Derived(); // 基类指针指向派生类对象 b->speak(); // 运行时调用派生类的实现,输出: "Derived speaking" delete b; return 0; }
-
虚函数(virtual):基类的成员函数在派生类中可以被重写,以实现多态。virtual 关键字确保了通过基类指针或引用调用该函数时,实际调用的是派生类中的函数。
-
运行时绑定:当程序运行时,根据对象的实际类型(派生类类型)决定调用哪个版本的 speak 函数。
-
虚析构函数:如果基类有虚函数,最好将析构函数也声明为虚函数,以确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免内存泄漏。
-
多态的对象切换:通过基类指针或引用操作派生类对象是多态的核心应用。
class Shape { public: virtual void draw() = 0; // 纯虚函数,接口方法 virtual ~Shape() {} // 虚析构函数 }; class Circle : public Shape { public: void draw() override { cout << "Drawing Circle" << endl; } }; class Rectangle : public Shape { public: void draw() override { cout << "Drawing Rectangle" << endl; } }; int main() { Shape* shape1 = new Circle(); Shape* shape2 = new Rectangle(); shape1->draw(); // 输出: Drawing Circle shape2->draw(); // 输出: Drawing Rectangle delete shape1; delete shape2; return 0; }
-
协变: 基类与派生类虚函数的返回值类型不同。
class A{ public: virtual int* print(){} }; class B: public A{ virtual float* print(){} };
-
基类的析构函数是虚函数,派生类只要定义析构函数,就构成覆盖(重写),无论加不加
virtual
,由编译器统一处理。class A{ virtual A(){}; virtual ~A(){}; }; class B: public A{ public: B(){} ~B(){} };
-
接口继承:虚函数的继承,目的是为了重写,达成多态。
实现继承:普通函数的继承。 -
静态绑定:编译期间确定程序行为(重载)。
动态绑定:程序运行期间,根据行为调用对应函数。(动态多态)
图片转自C++多态之虚函数表详解。
虚函数表:是指在每个包含虚函数的类中都存在着一个虚函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。
位置:虚函数表在编译阶段生成,存储于只读数据段(.rodata)。class A { public: virtual void v_a(){} virtual ~A(){} int64_t _m_a; }; int main(){ A* a = new A(); return 0; }
图片转自虚函数和多态。
-
不能成为虚函数的函数:
(1)普通函数(非成员函数):只有类的成员函数才有可能被声明为虚函数。
(2)静态函数:静态成员函数是编译时确定的,无法动态绑定,不支持多态。
(3)构造函数:调用构造函数才能初始化对象和虚表指针,调用虚函数前必须要先知道虚表指针,自相矛盾。
(4)内联函数:编译时展开,虚函数是运行时动态绑定,差异大。
(5)友元函数:友元函数无法被继承。 -
对象访问普通函数和虚函数的效率:
(1)普通对象一样快。
(2)指针/引用对象访问,普通函数更快。虚函数要在虚表内查询,访问效率降低。
28. 模版类、模版函数
C++ 中的模板(template)是一种泛型编程技术,它允许我们编写对多种数据类型适用的代码。模板主要包括两种类型:模板函数和模板类。
29. 深、浅拷贝
浅拷贝(位拷贝):浅拷贝只会复制指针的值,而不会复制指针所指向的内存。多个对象共用同一份资源,共用一片地址空间。当一个对象销毁,资源被释放,会引起访问出错。
深拷贝:拷贝数据时,先开辟一片新的地址空间,再将数据拷贝过来。
应用场景:
(1)动态分配内存:如果你的类包含指针并且动态分配了内存,那么应该使用深拷贝,确保每个对象都拥有独立的资源。
(2)避免共享资源:当你希望对象之间不共享资源,特别是当这些资源会被修改或释放时,应使用深拷贝。
写时拷贝:在构造时,拷贝的资源采用了引用计数,当计数变为0/1时,资源才被释放。触发条件:在修改数据时才触发,不修改就共享(利用拷贝构造)。
缺陷:C++标准认为,当你通过迭代器或者[]获取到string内部地址时,string分不清你是要读还是要写,当你获取到内部引用时,为了避免不能捕获你的操作,它会在此时停止写时拷贝。
注: 使用copy on write 时,不要获取string 内部的修改,千万不要通过[]和迭代器获取字符串内部地址引用,否则可能引用失效。
30. 二叉搜索树
31. 红黑树
32. AVL树
33. 哈希
34. STL
35. C++ 11
36. lambda 表达式
37. 进程与线程
38. C++ 线程库
39. B/B+树
40. 异常
41. 程序运行四个阶段
🔥程序运行的四个阶段:预处理,编译,汇编,链接。
- 预处理: 展开所有代码。
- 删除define,展开所有的宏定义。
- 处理所有的条件预编译指令,比如:#ifdef,#elif,#else,以及#endlif等。
- 引入所有的
#include
指令(头文件),将包含文件插入到预编译指令的位置。 - 去掉所有的注释。
- 添加行号和文件标识,以便编译的时候产生调试的行号以及编译错误警告行号。
- 保留所有的
#pragma
编译指令,以便编译器使用。
- 编译: 进行代码分析,没有错误则转为汇编语言。
- 词法分析(扫描):扫描器(scanner)将源代码的字符序列分割成一系列记号(token)。lex工具可以实现扫描(词法扫描)。
- 语法分析:语法分析器将记号(token)产生语法树(syntax Tree)。YACC(Yet Another Compiler Compiler)工具实现语法分析。
- 语义分析:静态语义(在编译阶段可以确定的语义),动态语义(在运行期间才能确定的语义)。
- 源代码优化:源代码优化器(source code optimizer)将整个语法树转为中间代码(Intermediate code)。中间代码使得编译器被分为前端和后端。编译器前端负责产生中间代码,编译器后端负责将中间代码转化为目标机器代码。
-
汇编:将汇编指令解释为机器可识别指令。 将这些指令打包成一种叫做可重定位目标程序的格式,将结果保存在二进制目标文件中。
-
链接:将所有使用到的代码(.o文件,库文件等)打包在一起生成可执行文件。
静态链接 动态链接 说明 生成可执行程序的时候,将库中的代码直接写入可执行程序,这样程序运行就不需要加载了。 生成可执行程序时,不写入库中的代码,而是记录为函数符号信息表,生成的可执行程序较小,程序运行时,加载这些库到内存中。 优点 启动运行速度快 1.节省磁盘和内存
2. 模块更新速度快
3. 有利于程序扩展
4. 代码冗余小,可执行文件占用空间小缺点 1. 代码冗余,若干程序使用一个库
2. 内存和磁盘浪费
3. 模块更新成本大损失部分运行时的性能