本文针对已经对C++有一定了解,部分知识摘取自C++ primer plus,如有错误请指正。
想到哪写到哪
目录
3.6.1.2虚函数表(VTable)和虚函数指针(VPTR):
1.CV限定符
1.1const
-
用途:用于定义一个值不能被修改的变量或对象。
-
编译时 vs 运行时:
const
可以用于在 运行时 或 编译时 定义不可变的变量。- 如果使用
const
定义的是一个全局常量,它的值会在程序运行时确定,可能是编译时已知,也可能是在运行时才确定。
- 如果使用
-
限制:
const
只能确保值在定义之后不会被修改,但它并不保证该值一定在编译时计算。示例:
const int x = 10; // x 是一个运行时不可修改的常量 const int y = some_function(); // y 是一个不可修改的常量,值在运行时才确定
对于普通变量
const int MAX_SIZE = 100;//规范命名:常量命名全大写
对于全局变量
const限定符影响默认存储类型。默认情况下全局变量的链接性为外部,但是const的全局变量链接性为内部。所以全局const定义就像使用了static。
//函数外定义
extern const int states = 10; //链接性为外部的const常量
extern static const int states = 10; //同上,此时static是多余的
对于成员函数
const Stock& topval(const Stock& s) const{
return s;
}
//括号内:表明该函数不会修改被显示访问的对象
//括号后:表面该函数不会修改被隐式访问的对象
//返回值:由于该函数返回了两个const对象之一的引用,所以返回值也应该是const
常量函数参数
//防止函数修改n,通常与引用一起使用
void fun(const int& n){
}
常量成员函数
//保证函数不会修改调用对象
void classname::show(xxx) const;//定义
void show(xxx) const;//声明
对于指针
1. 指向常量的指针(const T* ptr
或 T const* ptr
)
- 形式:
const int* ptr
或int const* ptr
- 解释:指针可以指向不同的对象,但指向的对象(数据)是不可修改的。
- 例子:
const int* ptr;
int x = 10;
int y = 20;
ptr = &x; // 可以改变指针的指向
*ptr = 20; // 错误,不能修改指针指向的值
ptr = &y; // 合法,可以修改指针的指向
2. 常量指针(T* const ptr
)
- 形式:
int* const ptr
- 解释:指针本身是常量,不能指向其他地址,但指向的对象是可以修改的。
- 例子:
int x = 10;
int* const ptr = &x;
*ptr = 20; // 合法,可以修改指针指向的对象
ptr = &y; // 错误,不能修改指针的指向
3. 指向常量的常量指针(const T* const ptr
)
- 形式:
const int* const ptr
- 解释:指针本身是常量,不能改变指向的地址,指针指向的对象也是常量,不能修改。
- 例子:
int x = 10;
const int* const ptr = &x;
*ptr = 20; // 错误,不能修改指针指向的对象
ptr = &y; // 错误,不能修改指针的指向
4. 指向指针的常量指针(T** const ptr
)
- 形式:
int** const ptr
- 解释:
ptr
是一个常量指针,指向一个指针。该指针本身不能被修改,但可以修改它所指向的指针的内容。 - 例子:
int x = 10;
int y = 20;
int* p = &x;
int** const ptr = &p;
*ptr = &y; // 可以修改 ptr 指向的指针的值
ptr = &p; // 错误,不能修改常量指针 ptr
5. 常量指向常量指针(const T** ptr
)
- 形式:
const int** ptr
- 解释:指针
ptr
指向另一个指针,该指针指向的值是常量。 - 例子:
const int x = 10;
const int* p = &x;
const int** ptr = &p;
**ptr = 20; // 错误,不能修改 ptr 指向的指针的值
总结:
const
可以修饰指针本身(指针的地址),或者指针所指向的数据。const
放在类型左边时,修饰的是指针指向的对象,指针指向的对象不可变。const
放在类型右边时,修饰的是指针本身,指针的地址不可变。
constexpr
-
用途:用于定义在 编译时 就能确定的常量,确保表达式在编译时求值。
-
编译时:
constexpr
强制要求变量或表达式的值必须在编译时就可以确定。只有能够在编译时确定的值才能被声明为constexpr
。 -
应用场景:通常用于优化代码性能,减少运行时的计算成本,并且可以用于构建编译时的常量表达式。
-
函数:除了变量,
constexpr
还可以用来修饰函数。一个constexpr
函数必须在编译时能确定返回值,但在某些情况下,它也可以在运行时执行(如果调用时传入的是运行时的值)。示例:
constexpr int square(int x) { return x * x; } constexpr int result = square(10); // 编译时计算 result = 100
值的确定时间 运行时或编译时 编译时确定 函数支持 不能用于函数修饰 可用于修饰函数,保证在编译时执行 用于全局常量 可以是运行时计算结果 必须在编译时确定 常量表达式计算 不能保证常量在编译时计算 确保在编译时计算 灵活性 可以在运行时初始化 只能使用编译时常量进行初始化
1.2volatile
不稳定的
不进行编译优化,会从内存中取数据,而不是寄存器
某些情况,编译器会自动优化,修改变量存取过程。修改后,当一些变量存在cpu寄存器中时,编译器会让cpu直接从寄存器读取,而不是内存中。如果有一个线程对内存中该变量进行了修改,则cpu从寄存器读取而不是内存,就出问题。
tips:
mutable(可变的)
一个类中成员变量被定义成mutable,即使这个类是const,这个变量也可以修改
2.函数指针和指针函数
函数指针
函数地址:就是函数名
如何获取函数指针:
double pam(int)//函数
double (*pf)(int);//函数指针
使用:(*pf) 与 pam()相等
区分:
double (*pf)(int);//返回double的函数指针
double *pf(int);//返回double*的函数
指针函数
返回值时指针的函数
3.类
3.1定义
class MyClass:public BaseClass{ //public继承
private:
int myVar;
int* data = new int;
public:
void myFun()
//构造函数,特征:无返回值;函数名=类名;可以定义多个不同参数(多态)
MyClass(int value, int data){
int myVar = value;
*data = data
}
MyClass(){} //定义了有参无默认值的构造函数后,必须提供默认构造函数
//或者将有参构造函数增加默认值就表示该有参构造函数是默认构造函数
//拷贝构造函数,特征:无返回值;函数名=类名;参数为MyClass &other;
MyClass(const MyClass& other){
//深拷贝,对每个成员变量挨个拷贝
myVar = other.var;
*data = *(other.data);
}
//析构函数,特征:无返回值;函数名"~类名"
~MyClass(){
//释放资源
delete data;
}
//运算符重载,特征:返回值MyClass&;类名"operate重载的符号";
MyClass& operator=(const MyClass& other){
if(this != &other){
myVar = other.myVar;
delete data;
data = new int;
*data = *(other.data)
}
return *this;
}
};
3.2实例化
MyClass obj;
MyClass* obj = new MyClass();//new方式
3.3this和*this
this //对象地址
*this //对象本身
3.4封装
3中封装方式的区别:
3.5继承
3个继承方式的区别:
3.5.1菱形继承与虚继承
- 菱形继承是指在多重继承时,某个类同时继承自多个基类,而这些基类又有一个共同的祖先类。这就导致了在最底层类中,会存在多个祖先类的拷贝,导致属性和方法的二义性问题。
- 例如,类
B
和类C
都继承自类A
,类D
同时继承B
和C
,此时D
继承了A
的两份拷贝。
解决方法:虚继承
class A {
public:
int x;
};
class B : virtual public A {
};
class C : virtual public A {
};
class D : public B, public C {
};
tips:
虚继承会改变构造函数调用顺序:
在有虚继承的情况下,构造函数的调用顺序也会发生变化。在构造派生类时,虚基类的构造函数会首先被调用,然后是直接基类的构造函数,最后是派生类自己的构造函数。
3.6多态
多态是指相同的函数或运算符在不同的对象上表现出不同的行为。C++ 支持编译时多态(通过函数重载和运算符重载)和运行时多态(通过虚函数实现)。依靠虚函数和动态绑定实现。
3.6.1重写(覆盖)
重写父类虚函数。
3.6.1.1虚函数
使用virture关键字。派生类可以覆盖虚函数。通过使用指向基类对象的指针或者引用调用虚函数时,程序将根据运行实际对象类型来确定调用函数。
编译器对虚函数使用动态联编
动态联编:
静态联编:
虚函数列表:存放虚函数地址,当虚函数被重写,放入新的地址。
tips:
构造函数不能为虚
析构函数应该为虚
友元不能为虚
3.6.1.2虚函数表(VTable)和虚函数指针(VPTR):
- 虚函数表(VTable):每个包含虚函数的类都维护一个虚函数表(vtable),表中记录了该类中虚函数的实际地址。每个派生类都会有一个自己的虚函数表,用来存储它重写的虚函数的地址。
- 虚函数指针(VPTR):每个对象都会包含一个隐藏的指针(vptr),指向该对象所属类型的虚函数表。当你通过基类的指针或引用调用虚函数时,程序会通过 vptr 查找虚函数表,然后执行正确的函数版本。
- 动态绑定:动态绑定是指程序在运行时通过虚函数表决定调用哪个函数。这与静态绑定相对,静态绑定是在编译时确定调用的函数。
class Derived : public Base {
public:
void show() override { std::cout << "Derived class" << std::endl; }
};
3.6.1.3final
关键字
- 如果希望阻止派生类进一步重写某个虚函数,可以在虚函数声明时使用
final
关键字。例如:
class Derived : public Base {
public:
void show() override final { std::cout << "Derived class" << std::endl; }
};
3.6.1.4纯虚函数
virtual int fun()=0;//声明,特征多一个"=0"
如果在基类中将某个虚函数声明为纯虚函数(= 0
),则该类成为抽象类,无法实例化对象。派生类必须实现所有纯虚函数,才能实例化对象。
3.6.1.5抽象类和接口类
接口类:全部成员函数都是纯虚函数
3.6.2重载
在同一个作用域内,可以定义多个同名不同参数列表的函数。
1.函数名相同
2.参数列表必须不同(参数类型、数量、顺序)
3.返回类型可以相同可以不同
3.6.2.1参数列表(函数特征标)
1.参数列表即函数特征标(不包含返回值,所以返回类型可以不同)
2.参数加了引用和不加,特征标相同
3.6.2.2运算符重载
1.自定义数学运算
2.容器类和迭代器:使用运算符重载提供方便操作,如.运算
3.输入输出流:例如cout和cin就对"<<"进行重载以处理不同类型的输入
4.比较和排序:重载"<","=="等
5.实现迭代器功能:例如"++","--"
重载的限制
重载的限制:
1.重载后的运算至少有一个操作数时用户定义的类型
2.使用运算符时不得违反运算符原来的句法规则
3.不能创建新运算符
4.不能重载sizeof、"."、".*"、"::"、"?:"等运算符
5.以下运算符只能通过成员函数重载:"="、"()"、"[]"、"->"
运算符重载和友元
两种运算符重载
1.成员函数
Time Time::operator*(double m) const{
//用于实现time*m,其实是调用time*(m)
}
2.非成员函数(使用友元,调用Time的成员变量)
Time operator*(double m, const Time& t){
//实现m*time。调用m*(m, time),但是m不是成员函数,没有权限调用time,所以就需要友元
}
3.7构造函数
1.当你不写构造函数时,会提供默认构造函数。但是当你写了构造函数,就必须再写上默认构造函数
2.默认构造函数并不是就是无参,给有参构造函数加上默认值也是默认构造函数
3.7.1默认构造函数
- 定义:默认构造函数是没有参数或所有参数都有默认值的构造函数。
- 作用:它用于在创建对象时对成员变量进行初始化。如果没有显式定义构造函数,编译器会生成一个隐式的默认构造函数。
- 调用场景:当你创建一个类对象时,如果没有提供任何初始值,默认构造函数会被调用。例如:
3.7.2移动构造函数
(Move Constructor)(C++11 引入):
- 定义:移动构造函数用于通过右值引用(
&&
)从一个临时对象“移动”资源到新对象,而不是拷贝它们。移动构造函数会“窃取”资源,并将原对象置为安全状态(例如将指针置nullptr
)。 - 作用:它避免了临时对象的深拷贝,提高了性能,尤其是在处理大对象或动态分配资源时。
- 调用场景:当对象与临时对象结合时,移动构造函数会被调用。例如:
class MyClass {
public:
MyClass(MyClass&& other) noexcept { /* 移动构造函数 */ }//使用右值引用
};
MyClass obj1 = std::move(MyClass()); // 调用移动构造函数
3.7.3拷贝构造函数
3.8析构函数
1.资源释放
2.清理操作
3.继承关系下的调用顺序:派生类的析构函数会自动调用基类的虚构函数,逐层清理父类资源
4.析构顺序:
1.成员变量按照声明的逆序进行销毁
2.派生类先于基类进行销毁
tips:
1.只能有一个
2.可以自动生成
3.小心处理指针和资源释放
4.析构函数应该为虚
3.9深拷贝与浅拷贝
3.9.1拷贝构造函数
定义:用于将一个对象复制到新的对象之中。用于初始化过程中,包括按值传递参数
何时调用:新建一个对象并初始化为同类现有对象时。
如下四种情况都会调用:
1.StringBad ditto(motto);
2.StringBad metto = metto;
3.StringBad also = StringBad(motto);
4.StringBad * pStringBad = new StringBad(motto);
注意事项:
1.系统会自动生成默认拷贝构造函数和赋值运算符重载,但是当涉及动态内存分配需要自己写。
- 定义:拷贝构造函数用于通过已存在的对象来创建新的对象,其参数为对已存在对象的引用(通常是
const
引用)。 - 作用:它在创建新对象时拷贝已有对象的内容,通常通过成员变量的逐个拷贝完成。
- 调用场景:
- 将对象传递给函数时(按值传递)。
- 从函数返回对象时(按值返回)。
- 显式地创建对象的副本时。
程序在什么时候执行的是浅拷贝、深拷贝、=赋值运算?
1.值传递参数、以值返回对象、使用初始化列表进行对象初始化时都会调用拷贝构造函数
2.当两个同类型对象使用"="时,调用赋值运算符重载函数
3.10友元
1.友元在类中声明,但不是成员函数,不需要classname::
2.不是成员函数,但是可以访问成员
3.只在声明处加friend关键字,定义处不加
3.10.1友元函数
在一个类内部声明并定义的独立函数,但不属于该类的成员。通过friend关键字声明。可以直接访问该类的私有和保护成员。
使用场景:
1.两个类需要共享私有数据,将一个类的成员函数声明为另一个类的友元函数
2.重载二元运算符,通常将运算符重载函数声明为友元
3.10.2友元类
友元类指在一个类中声明另一个类为其友元
使用场景:
1.类共享数据
2.代理模式、迭代器模式等中会使用到
3.10.3友元成员函数
3.11表初始化
比普通初始化更严格,不允许缩窄
3.12其他
struct和class区别:
1.struct默认共有和公有继承,不自动生成构造函数
2.class默认私有和私有继承,自动生成构造函数
类自动生成的函数:
1.默认构造函数
2.默认析构函数
3.拷贝构造函数
4.赋值构造函数
5.地址运算符
4.内存泄漏
1.谨慎使用new和delete
2.使用智能指针
3.定期检查和测试
4.使用容器类和标准库
5.遵从编码规范
6.使用内存分析工具
5.智能指针
share_ptr:共享指针,使用引用计数器追踪有多少个指针指向同一个资源,只有当最后一个指针销毁时,才会释放资源。存在循环引用问题。
unique_ptr:独占指针确保只有一个指针可以访问该资源,会自动释放
weak_ptr:弱指针,没有计数器,解决循环引用问题。
6.堆和栈
1.分配方式:
2.空间大小限制
3.分配速度:
4.生命周期:栈自动;堆手动,处理不好会内存泄漏
5.数据访问方式:栈访问更快,内存地址是连续的;堆由内存管理分配,可能是不连续的
6.使用场景:栈一般是自动存储,保持局部变量、函数调用过程中的参数传递;堆动态(手动)创建,需要手动释放
tips:
自动存储
静态存储
动态存储
7.static
1.未被初始化的静态变量的所有位都被设置成0,称为零初始化
2.零初始化和常量表达式初始化统称为静态初始化
三种静态变量的作用域
int global = 1000; //所有文件,但需要extern
static int one = 1; //本文件内
void fun(){
static int two = 2;
} //
静态成员变量
在类中:
static m_data; //所有类共享一个
const static m_data; //所有类共享一个且不可修改
1.所有类公用一个静态变量
2.静态成员变量需要在类外单独初始化
静态成员函数
1.只与类关联,不需要实例化就可调用,并可以直接访问静态成员函数
8.new和delete
需要手动释放,不要多次delete
在函数调用中的分配与释放需要注意的情况:
分配与释放
//分配
int* ptr = new int;
double* arr = new double[10];
//释放
delete ptr;
delete[] arr;
ptr = nullptr; //置空防止野指针
tips:
C++中的arr:长度固定
vector:长度可扩展
new和malloc
1.成功时new返回具体指针,malloc返回*void
2.失败时new返回异常,malloc返回null
3.是否需要指定内存大小
4.new/delete可重载,malloc/free是函数
5.new/delete会调用构造析构函数
6.delete时,new自动将指针指向nullptr;malloc没有
7.new和delete是运算符
9.引用与指针
引用定义
1.引用 = 别名
2.必须初始化,不能为空,一经初始化后不可改变
3.不支持地址运算
4.又叫左值引用(还有一个右值引用,用于移动语义)
将引用参数声明为const
1.避免无意中修改数据
2.使函数可以处理const和非const实参,否则只能接受非const
3.使函数能够正确生成并使用临时变量
当返回值为引用时
1.避免返回函数内定义的临时变量
2.应该返回参数传进来的引用,并且形参使const时,返回值也应该是const
3.或者返回该函数new出来的,但是必须记得在函数外delete,否则内存泄漏
10.模板
C++特性之一:泛型编程
定义
//模板类
template <typename T>
class Container{
private:
T data;
public:
Container(T value):data(value){} //新特性,列表初始化
void print(){
std::cout << "Data:" << data << std::endl;
}
};
//模板函数
template <typename T>
T maximum(T a, T b){
return (a > b ) ? a : b;
}
11.异常处理
1.
2.
12.STL常用容器
1.vector
2.list
3.deque
4.map
5.set
6.unorder_map
7.unorder_set
tips:
stack和queue是容器适配器,可指明由其他容器作为底层实现
13.迭代器
用于遍历容器的抽象概念,可以理解为指针。
声明
std::vector<int>::iterator it;
其他迭代器
begin() //第一个元素
end() //超尾,指向最后一个元素的下一个
使用auto it 简化操作
13.1迭代器失效
迭代器失效是指当容器的结构发生变化时,已有的迭代器可能不再有效,访问它们可能导致未定义行为(如崩溃或数据错误)。迭代器失效是 STL 中容器操作时需要特别注意的问题。
迭代器失效的常见原因:
-
容器元素的增加或删除:
- 向容器添加元素:某些容器在插入新元素时,可能会重新分配内存或改变内部结构,导致已有迭代器失效。
- 删除元素:从容器中删除元素通常会使指向被删除元素的迭代器失效,有些情况下还会影响其他迭代器。
-
容器的大小调整:
- 当容器的大小发生变化(例如
std::vector
增加容量时),容器可能重新分配其存储空间,从而导致所有指向该容器的迭代器失效。
- 当容器的大小发生变化(例如
-
容器的清空:
- 调用
clear()
清空容器后,所有指向容器的迭代器都失效。
- 调用
具体容器中的迭代器失效规则:
std::vector:
插入元素:如果插入操作引发重新分配(如容量不足时),所有迭代器都会失效;如果没有重新分配,则插入点之后的迭代器失效。
删除元素:调用 erase() 或 pop_back() 删除元素时,删除点及其之后的所有迭代器失效。
避免方法:尽量预先调用 reserve() 来分配足够的内存,避免插入时频繁触发内存重新分配。
std::list:
插入元素:在 std::list 中插入元素不会导致已有迭代器失效,因为 std::list 是链表结构,插入不涉及大规模内存移动。
删除元素:只有指向被删除元素的迭代器失效,其他迭代器不会失效。
std::deque:
插入和删除元素:在两端插入或删除时可能会导致所有迭代器失效。在中间插入或删除则可能导致插入点及其之后的迭代器失效。
std::map 和 std::set:
插入元素:插入不会使任何迭代器失效。
删除元素:只有指向被删除元素的迭代器失效。
std::unordered_map 和 std::unordered_set:
插入元素:当装载因子超过阈值时,容器会重新分配内部哈希表,所有迭代器失效。
删除元素:只有指向被删除元素的迭代器失效。
std::string:
类似于 std::vector,插入或删除操作时可能会导致迭代器失效,特别是当存储重新分配时。
14.命名空间
1.避免命名冲突
2.组织代码
3.全局声明隔离
使用
1.using namespace std放在函数定义之前,让文件中所有函数都能用
2.using namespace std放在函数内
3.在函数内使用using std::cout
4.直接使用std::cout
15.预处理器
在编译前对代码进行一系列文本替换和指令处理,通常以"#"开通,不是正真的C++代码。
文件包含
#include <iostream>
#include "../../xxx"
C++与C的include的区别:
1.新式C++没有扩展名
2.C中#include<math.h>
C++中#inclde<cmath>
宏定义
#define MAX(a, b) ((a) > (b)> ? (a) :(b))
为什么要加这么多括号?因为宏定义只是进行简单的字符替换,非常容易出问题
宏定义函数和内联函数的区别:
1.
2.
条件编译
1.#ifdef
2.#ifndef
3.#if
16.文件操作
文件写入
文件读取
追加写入
17.指针与数组
int arr[] = {1, 2, 3, 4};
int* ptr = arr;
1.sizeof(arr)是数组长度;sizeof(ptr)是地址长度
2.arr是常量,ptr可修改
3.arr是第一个元素地址
4.++运算每次加一个类型的长度,即指向下一个元素地址
5.当arr作为参数传入函数时,sizeof(arr)是地址长度即4,所以当传数组名arr时一般还会传个数组长度n
18.排序算法
1.冒泡
2.选择
3.插入
4.快排
5.归并
6.堆排
19.设计模式
单例模式
观察者模式
工厂模式
适配器模式
策略模式
装饰器模式
模板方式模式
20.线程
C++-一文看懂C++多线程编程和各种锁机制_concurrentqueue c++-CSDN博客
21.线程同步
互斥锁
22.内联函数
将内联函数插入调用点提高程序运行效率,减少上下文切换的开销。
1.声明和定义前加inline
2.内联不能递归
3.虚拟成员函数无法内联
4.定义于类声明中的函数都自动称为内联函数
内联与宏定义区别
1.内联函数是在编译过程中,编译器会进行检查,如果过于复杂会放弃内联,当做普通函数执行
2.宏定义是编译前的预处理,只进行简单字符替换,不进行检查
-
作用:
inline
关键字提示编译器将函数定义替换到每个调用的地方,避免进入函数的上下文切换,从而提高小型、简单函数的执行效率。编译器会将内联函数的函数体插入到调用该函数的代码处,而不是生成函数调用的开销。- 编译器优化:虽然使用
inline
关键字,但编译器可能会根据函数的复杂性选择是否真正将其内联,未必强制执行。
-
局限性:
- 内联函数不应该太长,通常适合短小、简单的函数。如果函数中有复杂的控制流(如
for
循环、if-else
分支、递归等),编译器通常不会进行内联优化。 - 递归函数无法内联,因为递归需要不断调用自身,不能直接替换成代码块。
- 内联函数不应该太长,通常适合短小、简单的函数。如果函数中有复杂的控制流(如
-
与宏的区别:
- 内联函数:是 C++ 语言中的一种函数,具备类型检查和作用域控制。它在编译时生效,会进行参数类型和返回值的检查,从而避免类型不匹配的错误。
- 宏定义:通过
#define
定义的宏是由预处理器进行简单的文本替换,没有类型检查,可能导致一些隐藏的错误和调试困难。
默认内联函数:
- 当你在类的内部定义函数时,编译器会自动将这些函数视为内联函数。例如:
23.动态绑定
即运行时多态,在程序运行时根据实际对象类型决定调用哪个方法或函数。
24.移动语义
右值引用(Rvalue Reference):
- 右值引用是通过
&&
符号定义的,允许绑定到右值(临时对象或没有名字的对象)。右值引用的主要目的是实现移动语义,从而避免不必要的拷贝。 - 右值引用可以有效地**“窃取”**临时对象的资源,而不必创建深拷贝。这样可以提高程序的效率,特别是在处理大数据结构时。
- 例如,右值引用在移动构造函数中非常有用:
class MyClass {
private:
int* data;
public:
MyClass(int size) : data(new int[size]) {}
~MyClass() { delete[] data; }
// 移动构造函数,使用右值引用避免不必要的拷贝
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 确保原对象不再拥有资源
}
};
移动语义(Move Semantics):
- 移动语义允许将资源从一个对象转移到另一个对象,而不进行复制。在没有移动语义之前,如果要将一个临时对象的资源赋值给另一个对象,C++ 需要进行深拷贝,这样就增加了不必要的开销。
- 移动语义主要通过移动构造函数和移动赋值运算符来实现,使用右值引用来接受临时对象的资源。
- 比如:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 的资源移动给 v2
25.类型转换
隐式类型转换
int num = 3.14;
explicit关键字
explicit关键字用于关闭隐式转换,只允许显示类型转换,防止不必要的问题
c++98中explicit关键字不能用于类型转换
c++11支持
显示转换
C风格强转
int num = (int)3.14;
tips:
int强转是四舍五入,不是去掉小数部分
函数风格强转
int num = int(3.14);
dynamic_cast转换
//执行类层次间的安全向下转型或基类到派生类的向上转型
dynamic_cast<type>(expression); //运行时进行类型检查
自定义类型转换函数
类中可以定义转换函数
1.转换函数必须是类的方法
2.不能指定返回类型
3.不能有参数
class MyType{
private:
int value;
public:
MyType(int v = 0) : value(v){}
operator int() const{
return value;
}
};
int main(){
MyType obj(42);
int num = obj; //隐式调用
int num1 = static_cast<int>(obj); //显示调用
return 0;
}
26.尾递归
27.浮点运算精度问题
阈值法
double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 1e-6;
if(std::abs(a - b) < epsilon){
//在可接受范围内
}
比较法
double a = 0.1 + 0.2;
double b = 0.3;
double relative_error = std::abs((a - b) / b);
if(relative_error < 0.01){
//误差在可接受范围内
}
28.数组
1.只有在定义时才能初始化
int cards[4] = {1, 2, 3, 4};
2.如果初始化给的数量小于长度,其他默认为0
int cards[10] = {0};
3.不指定长度
int cards[] = {1, 2, 3, 4};
C++特性
1.可以省略"="
int cards[4] {1, 2, 3, 4};
2.可以{}无任何值表示默认为0
int cards[10] = {};
3.禁止缩窄操作
29.字符串
1.sizeof():字节长度
2.strlen():可见字符长度
C风格
1."\0"结尾
char dog[3] = {'d', 'o', 'g'}; //可修改,长度为4,默认\0
char dog[] = "dog"; //常量,不可修改,长度为4
tips:
strlen()和sizeof()区别
string
30.野指针和悬浮引用
野指针:指向已经释放或内存无效的指针
1.delete后未置空
2.返回局部变量的指针
3.函数参数指针被释放
悬浮引用:指向已被销毁对象的引用
31.内存对齐
数据在内存中存储起始地址是某个值的倍数
提高cpu访问效率,减少内存访问次数
tips:
1.结构体和共同体的内存对齐,共用体长度是最大类型长度
2.int类型是计算机语言标准长度,是cpu处理最快的类型
32.char
1.默认情况下即不是无符号也不是有符号
2.可以显示指定有无符号
unsigned char c;
signed char c;
33.存储持续性、作用域和链接性
1.自动存储持续性
2.静态存储持续性
3.线程存储持续性
4.动态存储持续性
作用域描述名称在文件的多大范围内可见。
1.局部:代码块内可见
2.全局(文件作用域):定义位置到文件结尾处
链接性
1.外部链接性:可在其他文件访问
2.内部链接性:当前文件内访问。每个文件都有自己的一组常量,每个定义都是文件私有
3.无链接性:只能在当前函数或者代码块中访问
存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用static |
1.链接性为外部的称为外部变量,又称为全局变量
2.定义声明:简称定义,分配存储空间
3.引用声明:简称声明,不分配内存,引用已有变量,使用关键字extern
4.const会将全局变量的链接性改为内部,即
const int fingers = 10; //same as static const int fingers = 10
//每一个include该文件都会有单独一个fingers
//如果想要一个全局(链接性为外部)的常量,使用如下:
extern const int states = 50;
函数的链接性
1.由于不允许函数嵌套定义,所以函数的存储持续性为静态,链接性为外部,即可以在文件间共享。
2.可以在函数前加上extern指出函数实在另一个文件定义,extern用于将内部链接性覆盖为外部链接性
3.可以在函数前增加static将函数链接性改为内部,这样只能在一个文件内使用
34.Lambda表达式
匿名函数
定义
[capture list] (parameters) -> return_type{
//函数体
}
capture list:捕获列表,用于指定lamda中所使用的外部变量
35.内存管理方式
1.自动存储:自动变量存于栈
2.静态存储:定义函数外的;static的;整个程序执行期间都存在
3.动态存储:
36.结构体和类区别
下面代码定义一个链表,使用到结构体和类,除了书中介绍,发现使用中还有一些区别。
1.可以在类里定义一个结构体
2.C++中结构体和类的默认访问权限是不一样的,一个默认private,一个默认public;C无访问限制
3.C++中是否生成默认构造函数析构函数等有区别
4.C++里结构体可以使用构造函数初始化,C无
5.C语言中使用结构体时,如果没有在结构体定义时写别名,是需要加struct。C++则不用
6.C语言不能有函数,C++可以定义成员函数
7.C++中class可继承,struct无
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next; //C语言这里需要用struct LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}//定义有参的默认构造函数
};//C语言如果这里没设置别名(前面加typedef,后面加别名),下面都要加struct
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); //这里初始化必须加参,因为默认构造函数是有参的
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
37.协程
处于用户态的轻量级线程,即一个线程包括多个协程。因为处于用户态,系统不管,需要手动调度
协程与线程比较
1.一个线程可以多个协程,一个进程也可以单独拥有多个协程。
2.线程进程都是同步机制,而协程则是异步。
3.协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
tips:
进程:系统资源分配最小单位;三个状态: ;调度算法;死锁(原因/避免);上下文;
线程:cpu调度最小单位;资源共享;
38.未定义行为
未定义行为(Undefined Behavior, UB) 是指程序的行为超出了 C++ 标准的定义范围,编译器对此不做任何保证,可能会出现意外的运行结果、程序崩溃,甚至无法预测的行为。C++ 标准没有规定对未定义行为的处理方式,因此在不同的平台、编译器或运行时环境中,未定义行为的表现可能各不相同。
常见的未定义行为:
1数组越界访问: 访问数组中不属于有效范围的元素会引发未定义行为。
2使用未初始化的变量: 读取未初始化的变量(尤其是局部变量)的值也是未定义行为。它们可能包含垃圾值,程序运行结果不可预测。
3整数溢出(对于有符号整数): 对于有符号整数,超出其最大表示范围时(例如从正值变为负值),会导致未定义行为。
4空指针解引用: 解引用空指针(即指向 nullptr 或 NULL 的指针)会导致未定义行为,通常会导致程序崩溃。
5悬空指针访问: 使用已经释放或未分配的内存地址(悬空指针)也会导致未定义行为。
6重复释放内存: 对同一块动态分配的内存多次调用 delete 会产生未定义行为。
7违反类型别名规则: 将一个类型的指针强制转换为另一种不兼容的类型并进行访问,会引发未定义行为。
8数据竞争(多线程环境下): 多线程环境下,多个线程同时读取和写入同一个变量,且没有同步措施,可能导致未定义行为。数据竞争的结果是不可预测的。
9位移操作溢出: 对整数进行超出其位数的位移操作会产生未定义行为。例如,试图将 32 位的整数向左移位 33 位。
10违反常量指针的常量性: 强行修改通过 const 修饰的指针或对象的值也是未定义行为。