赞
踩
总结c++语法、内存等知识。仅供个人学习记录用
指针存放某个对象的地址,其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变
引用就是变量的别名,从一而终,不可变,必须初始化
*
符号。&
符号。什么是函数指针,如何定义和使用场景
函数指针是指向函数的指针变量。它可以用于存储函数的地址,允许在运行时动态选择要调用的函数。
格式为:返回类型 (*指针变量名)(参数列表)
幅值方法: 指针名 = 函数名; 指针名 = &函数名
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 定义⼀个函数指针,指向⼀个接受两个int参数、返回int的函数
int (*operationPtr)(int, int);
// 初始化函数指针,使其指向 add 函数
operationPtr = &add;
// 通过函数指针调⽤函数
int result = operationPtr(10, 5);
cout << "Result: " << result << endl;
// 将函数指针切换到 subtract 函数
operationPtr = &subtract;
// 再次通过函数指针调⽤函数
result = operationPtr(10, 5);
cout << "Result: " << result << endl;
return 0;
}
使用场景:
函数指针和指针函数的区别
函数指针是指向函数的指针变量。可以存储特定函数的地址,并在运行时动态选择要调用的函数。通常用于回调函数、动态加载库时的函数调用等场景。
int add(int a, int b) {
return a + b;
}
int (*ptr)(int, int) = &add; // 函数指针指向 add 函数
int result = (*ptr)(3, 4); // 通过函数指针调⽤函数
指针函数是⼀个返回指针类型的函数,⽤于返回指向某种类型的数据的指针。
int* getPointer() {
int x = 10;
return &x; // 返回局部变ᰁ地址,不建议这样做
}
C++ 整型数据长度标准:
short 至少 16 位
int 至少与 short ⼀样长
long 至少 32 位,且至少与 int ⼀样长
long long 至少 64 位,且至少与 long ⼀样长
在使用8位字节的系统中,1 byte = 8 bit。
很多系统都使用最小长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,int 的长度较为灵活,⼀般认为 int 的长度为 4 个字节,与 long 等长。
可以通过运算符 sizeof 判断数据类型的长度。例如sizeof (int)
头文件climits定义了符号常量:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值
即为不存储负数值的整型,可以增大变量能够存储的最大值,数据长度不变。
int 被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时⼀般选用 int 类型。
关键字:static_cast、dynamic_cast、reinterpret_cast和 const_cast
static_cast
dynamic_cast
reinterpret_cast
const_cast
不能直接用==来判断,会出错!
对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!
浮点数与0的比较也应该注意。与浮点数的表示方式有关。
const的作用
const 关键字主要用于指定变量、指针、引用、成员函数等的性质
常量指针(底层const)
是指定义了一个指针,这个指针指向⼀个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是指针对其所指对象的不可改变性。
特点:靠近变量名。
形式:
(1)const 数据类型 * 指针变量 = 变量名
(2)数据类型 const * 指针变量 = 变量名
int temp = 10;
const int* a = &temp;
int const *a = &temp;
// 更改:
*a = 9; // 错误:只读对象
temp = 9; // 正确
指针常量(顶层const)
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。指针常量强调的是指针的不可改变性。
特点:靠近变量类型。
形式:数据类型 * const 指针变量 = 变量名
int temp = 10;
int temp1 = 12;
int* const p = &temp;
// 更改:
p = &temp2; // 错误
*p = 9; // 正确
拓展:
顶层const:指针本身是常量;
底层const:指针所指的对象是常量;
左定值,右定向:指的是const在*的左还是右边
const在*左边,表示不能改变指向对象的值,常量指针;
const在*右边,表示不能更换指向的对象,指针常量
若要修改const修饰的变量的值,需要加上关键字volatile;
若想要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;
static关键字主要用于控制变量和函数的生命周期、作用域以及访问权限。
实现多个对象之间的数据共享 + 隐藏,并且使用静态成员还不会破坏隐藏原则;
void exampleFunction() {
static int count = 0; // 静态变量
count++;
cout << "Count: " << count << endl;
}
class ExampleClass {
public:
static int staticVar; // 静态成员变量声明
};
// 静态成员变量定义
int ExampleClass::staticVar = 0;
class ExampleClass {
public:
static void staticMethod() {
cout << "Static method" << endl;
}
};
void exampleFunction() {
static int localVar = 0; // 静态局部变量
localVar++;
cout << "LocalVar: " << localVar << endl;
}
define
define:
定义预编译时处理的宏,只是简单的字符串替换,没有类型检查,不安全。
inline:
inline是先将内联函数编译完成生成了函数体,直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;
内联函数是一种特殊的函数,会进行参数类型检查;
对编译器的一种请求,编译器有可能拒绝这种请求;
C++中inline编译限制:
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
const 表示“只读”的语义,constexpr 表示“常量”的语义
constexpr 只能定义编译期常量,而const 可以定义编译期常量,也可以定义运行期常量。
你将一个成员函数标记为constexpr,则顺带也将它标记为了const。如果你将⼀个变量标记为constexpr,则同样它是const的。但相反并不成立,一个const的变量或函数,并不是constexpr的。
constexpr变量
复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
必须使用常量初始化:
constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007;
如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关。
constexpr int *p = nullptr; //常量指针 顶层const
const int *q = nullptr; //指向常量的指针, 底层const
int *const q = nullptr; //顶层const
constexpr函数:
constexpr函数是指能用于常量表达式的函数。
函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。
constexpr int new() {return 42;}
为了可以在编译过程展开,constexpr函数被隐式转换成了内联函数。
constexpr和内联函数可以在程序中多次定义,一般定义在头文件。
constexpr 构造函数:
构造函数不能说const,但字面值常量类的构造函数可以是constexpr。
constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用 constexpr 修饰
constexpr的好处
影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,而不是直接从寄存器拷贝内容。
多线程中被几个任务共享的变量需要定义为volatile类型。
volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里修改一些跟类状态无关的数据成员,那么这个成员就应该被mutable来修饰。
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数⾥值也可以被修改
public:
void add() const//在函数⾥不可修改this指针指向的值 常量指针
{
m_A=10;//错误 不可修改值,this已经被修饰为常量指针
m_B=20;//正确
}
}
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数⾥值也可以被修改
}
int main()
{
const person p;//修饰常对象 不可修改类成员的值
p.m_A=10;//错误,被修饰了指针常量
p.m_B=200;//正确,特殊变量,修饰了mutable
}
explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换,注意:
定义:声明外部变量(在函数或者文件外部定义的全局变量)
对于**extern"C"**的使用场景
(1)C++代码中调用C语言代码;
(2)在C++中的头文件中使用;
(3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;
self &operator++() { //前置++
node = (linktype)((node).next);
return *this;
}
const self operator++(int) { //后置++
self tmp = *this;
++*this;
return tmp;
}
为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默默给int指定为⼀个0
问题:a++ 和 int a = b 在C++中是否是线程安全的?
答案:不是!
a++:
从C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,其实不是原子的。
其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的内存中
mov eax, dword ptr [a] # (1)
inc eax # (2)
mov dword ptr [a], eax # (3)
现在假设a的值是0,有两个线程,每个线程对变量a的值都递增1,预想⼀下,其结果应该是2,可实际运行结构可能是1!是不是很奇怪?
int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {a++;}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {a++;}
我们预想的结果是线程1和线程2的三条指令各自执行,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1执行完指令(1)和(2)后,eax寄存器中的值变为1,此时操作系统切换到线程2执行,执行指令(3)(4)(5),此时eax的值变为1;接着操作系统切回线程1继续执⾏,执行指令(6),得到a的最终结果1。
int a = b
从C/C++语法的级别来看,这是条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址
中:
mov eax, dword ptr [b]
mov dword prt [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程而出现不确定的情况。
解决办法
C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是⼀个模板类型:
template<class T>
struct atomic:
我们可以传⼊具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型
// 初始化1
std::atomic<int> value;
value = 99;
// 初始化2
// 下⾯代码在Linux平台上无法编译通过(指在gcc编译器)
std::atomic<int> value = 99;
// 出错的原因是这⾏代码调⽤的是std::atomic的拷贝构造函数
// ⽽根据C++11语⾔规范,std::atomic的拷贝构造函数使⽤=delete标记禁止编译器⾃动⽣成
// g++在这条规则上遵循了C++11语言规范。
void exampleFunction() {
static int count = 0; // 静态局部变ᰁ
count++;
cout << "Count: " << count << endl;
}
int globalVar = 10; // 全局变量
void function1() {
globalVar++;
}
void function2() {
globalVar--;
}
生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。
操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。
全局变量(外部变量)的说明之前再冠以static就构成了静态的全局变量。
全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。
这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。
static全局变量与普通的全局变量的区别是static全局变量只初始化一次,防止在其他文件单元被引用。
static函数与普通的函数作用域不同。尽在本文件中。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。
对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。
static函数与普通函数最主要区别是static函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)
常见异常有:数组下标越界、除法计算时除数为0、动态分配空间时空间不⾜
如果不及时对这些异常进行处理,程序多数情况下都会崩溃。
C++中的异常处理机制主要使用try、throw和catch三个关键字,其在程序中的用法如下:
#include <iostream>
using namespace std;
int main()
{
double m = 1, n = 0;
try {
cout << "before dividing." << endl;
if (n == 0)
throw - 1; //抛出int型异常
else if (m == 0)
throw - 1.0; //拋出 double 型异常
else
cout << m / n << endl;
cout << "after dividing." << endl;
}
catch (double d) {
cout << "catch (double)" << d << endl;
}
catch (...) {
cout << "catch (...)" << endl;
}
cout << "finished" << endl;
return 0;
}
//运⾏结果
//before dividing.
//catch (...)
//finished
代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。
catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。
int fun() throw(int,double,A,B,C){...};
这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常
静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
目标文件
可执行目标文件:可以直接在内存中执行;
可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;
静态库有以下两个问题:
当静态库更新时那么整个程序都要重新进行链接;
对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。
共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点:
在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
在内存中,⼀个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。
coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。
使用gdb命令对core文件进行调试
C++程序运行时,内存被分为几个不同的区域,每个区域负责不同的任务。
栈和堆都是用于存储程序数据的内存区域。
栈是一种有限的内存区域,用于存储局部变量、函数调用信息等。堆是一种动态分配的内存区域,用于存储程序运行时动态分配的数据。
栈上的变量生命周期与其所在函数的执行周期相同,而堆上的变量生命周期由程序员显式控制,可以(使用 new 或 malloc )和释放(使用 delete 或 free )。
栈上的内存分配和释放是自动的,速度较快。而堆上的内存分配和释放需要手动操作,速度相对较慢。
\ | 堆 | 栈 |
---|---|---|
管理方式 | 堆中资源由程序员控制(容易产生memory leak) | 栈资源由编译器自动管理,无需手工控制 |
内存管理机制 | 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中) | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(理解一下连续空间和不连续空间的区别、链表和队列的区别,从而理解两种机制不同) |
空间大小 | 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大 | 栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置) |
碎片问题 | 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 | 对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。 |
生长方向 | 堆向上,向高地址方向增长。 | 栈向下,向低地址方向增长。 |
分配地址 | 堆都是动态分配(没有静态分配的堆) | 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 |
分配效率 | 堆由C/C++函数库提供,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,机制很复杂。所以堆的效率比栈低很多。 | 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令。效率比较高。 |
智能指针用于管理动态内存的对象,其主要目的是在避免内存泄漏和方便资源管理。
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
#include <memory>
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;
#include <memory>
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
1、new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。(只对于plain new,nothrow new会返回NULL)
2、使用new操作符申请内存分配时无须指定内存块的大小,而malloc则需要显式地指出所需内存的尺寸。
3、opeartor new /operator delete可以被重载,而malloc/free并不允许重载。
4、new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会,只是分配和释放内存块
5、malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
6、new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
7、delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。free 不会修改指针的值,可能导致野指针问题。
8、delete 可以正确释放通过 new[] 分配的数组。free 不了解数组的大小,不适用于释放通过 malloc 分配的数组。
不是,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
因此plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的。
void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
void* operator new(size_t,void*);
void operator delete(void*,void*);
palcement new注意两点:
palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组
placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不⼀定等于原来分配的内存大小,使⽤delete会造成内存泄漏或者之后释放内存时出现运行时错误。
野指针是指指向已被释放的或无效的内存地址的指针。使⽤野指针可能导致程序崩溃、数据损坏或其他不可预测的行为。
int* ptr = new int;
delete ptr;
// 此时 ptr 成为野指针,因为它仍然指向已经被释放的内存
ptr = nullptr; // 避免野指针,应该将指针置为 nullptr 或赋予新的有效地址
int* createInt() {
int x = 10;
return &x; // x 是局部变量,函数结束后 x 被销毁,返回的指针成为野指针
}
// 在使用返回值时可能引发未定义⾏为
void foo(int* ptr) {
// 操作 ptr
delete ptr;
}
int main() {
int* ptr = new int;
foo(ptr);
// 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为野指针
// 避免:在 foo 函数中不要释放调用方传递的指针
}
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
野指针,指的是没有被初始化过的指针
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使⽤
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr ,这样在使用时编译器就会直接报错,产生的非法内存访问。
悬浮指针指的是指针最初指向的内存已经被释放了的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为 p=p2=nullptr 。此时再使用,编译器会直接保错。
如何避免
避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。
内存对齐是指数据在内存中的存储起始地址是某个值的倍数。
在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果⼀个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
比如在32位cpu下,假设⼀个整型变量的地址为0x00000004(为4的倍数),那它就是自然对齐的,而如果其地址为0x00000002(非4的倍数)则是非对齐的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
c++11以后引入两个关键字 alignas与 alignof。其中 alignof 可以计算出类型的对齐方式, alignas 可以指定结构体的对齐方式。
需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的⼀个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。
而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始,如果⼀个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
大多数计算机硬件要求基本数据类型的变量在内存中的地址是它们大小的倍数。例如,⼀个 32 位整数通常需要在内存中对齐到 4 字节边界。
内存对齐可以提高访问内存的速度。当数据按照硬件要求的对齐方式存储时,CPU可以更高效地访问内存,减少因为不对齐而引起的性能损失。
许多计算机体系结构使用缓存行(cache line)来从内存中加载数据到缓存中。如果数据是对齐的,那么一个缓存行可以装载更多的数据,提高缓存的命中率。
有些计算机架构要求原子性操作(比如原子性读写)必须在特定的内存地址上执行。如果数据不对齐,可能导致无法执行原子性操作,进而引发竞态条件。
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
1、 偏移量要是n和当前变量大小中较小值的整数倍
2、 整体大小要是n和最大变量大小中较小值的整数倍
3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
用于任务链(即任务A的执行必须依赖于任务B的返回值)
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
因为sizeof值在编译时确定,所以不能⽤来得到动态分配(运⾏时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的⻓度
strlen(str); // 取的是这个字符串的⻓度,不包含结尾的 \0。⼤⼩是4
return 0;
}
声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;
定义要在定义的地方为其分配存储空间。
相同变量可以在多处声明(外部变量extern),但只能在一处定义。
零拷贝就是⼀种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
零拷贝技术可以减少数据拷贝和共享总线操作的次数。
在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造
#include <iostream>
using namespace std;
int f(int n)
{
cout << n << endl;
return n;
}
void func(int param1, int param2)
{
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进⾏输出,输出结果则刚好
相反
}
int main(int argc, char* argv[])
{
func(1, 2);
return 0;
}
//输出结果
//2
//1
//var1=1,var2=2
当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、main函数中的变量、进行依次压栈;
当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;
当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈
从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。
函数的调用过程:
1)从栈空间分配存储空间
2)从实参的存储空间复制值到形参栈空间
3)进行运算
char str[] = "hello";
char* p = str;
int n = 10;
// 请计算
sizeof(str) = ?
sizeof(p) = ?
sizeof(n) = ?
void Func(char str[100]){
// 请计算
sizeof(str) = ?
}
void* p = malloc(100);
// 请计算
sizeof(p) = ?
参考答案:
sizeof(str) = 6;
sizeof()计算的是数组的所占内存的大小包括末尾的 ‘\0’
sizeof(p) = 4;
p为指针变量,32位系统下大小为 4 bytes
sizeof(n) = 4;
n 是整型变量,占用内存空间4个字节
void Func(char str[100]){
sizeof(str) = 4;
}
函数的参数为字符数组名,即数组首元素的地址,大小为指针的大小
void* p = malloc(100);
sizeof(p) = 4;
p指向malloc分配的大小为100 byte的内存的起始地址,sizeof§为指针的大小,而不是它指向内存的大小
void GetMemory1(char* p){
p = (char*)malloc(100);
}
void Test1(void){
char* str = NULL;
GetMemory1(str);
strcpy(str, "hello world");
printf(str);
}
char *GetMemory2(void){
char p[] = "hello world";
return p;
}
void Test2(void){
char *str = NULL;
str = GetMemory2();
printf(str);
}
void GetMemory3(char** p, int num){
*p = (char*)malloc(num);
}
void Test3(void){
char* str = NULL;
GetMemory3(&str, 100);
strcpy(str, "hello");
printf(str);
}
void Test4(void){
char *str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
cout << str << endl;
}
}
参考答案:
Test1(void):
程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的 str一直都是NULL。strcpy(str, “hello world”)将使程序奔溃
Test2(void):
可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,使其原现的内容已经被清除,新内容不可知。
Test3(void):
能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
Test4(void):
篡改动态内存区的内容,后果难以预料。非常危险。因为 free(str);之后,str成为野指针,if(str != NULL)语句不起作用。
char* strcpy(char* strDest, const char* strSrc);
参考答案:(函数实现)
char* strcpy(char *dst,const char *src) {// [1]
assert(dst != NULL && src != NULL); // [2]
char *ret = dst; // [3]
while ((*dst++=*src++)!='\0'); // [4]
return ret;
}
char s[10]="hello";
strcpy(s, s+1);
// 应返回 ello
strcpy(s+1, s);
// 应返回 hhello 但实际会报错
// 因为dst与src重叠了,把'\0'覆盖了
所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有一种情况: src<=dst<=src+strlen(src)
C函数 memcpy 自带内存重叠检测功能,下面给出 memcpy 的实现my_memcpy
char * strcpy(char *dst,const char *src)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
my_memcpy(dst, src, strlen(src)+1);
return ret;
}
/* my_memcpy的实现如下 */
char *my_memcpy(char *dst, const char* src, int cnt)
{
assert(dst != NULL && src != NULL);
char *ret = dst;
/*内存重叠,从⾼地址开始复制*/
if (dst >= src && dst <= src+cnt-1){
dst = dst+cnt-1;
src = src+cnt-1;
while (cnt--){
*dst-- = *src--;
}
}
else {//正常情况,从低地址开始复制
while (cnt--){
*dst++ = *src++;
}
}
return ret;
}
已知String的原型为:
class String
{
public:
String(const char *str = NULL);
String(const String &other);
~ String(void);
String & operate =(const String &other);
private:
char *m_data;
};
请编写上述四个函数
参考答案:
此题考察对构造函数赋值运算符实现的理解。实际考察类内含有指针的构造函数赋值运算符函数写法。
// 构造函数
String::String(const char *str)
{
if(str==NULL){
m_data = new char[1]; //对空字符串自动申请存放结束标志'\0'
*m_data = '\0';
}
else{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
//拷⻉构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operate =(const String &other)
{
if(this == &other){
return *this; // 检查自赋值
}
delete []m_data; // 释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length + 1]; //对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this; //返回本对象的引⽤
}
参考答案:
对于一个进程,其空间分布如下图所示:
如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。
参考答案:
如果是带有自定义析构函数的类类型,用new[]来创建类对象数组,而用delete来释放会发生什么?用例子来说明:
class A {};
A* pAa = new A[3];
delete pAa;
那么 delete pAa; 做了两件事:
定义:让某种类型对象获得另一个类型对象的属性和方法
功能:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
常见的继承有三种方式:
1、实现继承:指使用基类的属性和方法而无需额外编码的能力
2、接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
3、可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力
例如:
将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法。
**注意:**派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。所以基类必须定义而非声明。
定义:数据和代码捆绑在⼀起,避免外界干扰和不确定性访问;
功能:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。
定义:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
功能:多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作; 简单一句话:允许将子类类型的指针赋值给父类类型的指针。
实现多态有两种方式
public的变量和函数在类的内部外部都可以访问。
protected的变量和函数只能在类的内部和其派生类中访问。
private修饰的元素只能在类内访问。
一个类可以从多个基类(父类)继承属性和行为。在C++等支持多重继承的语言中,一个派生类可以同时拥有多个基类。
多重继承可能引入一些问题,如菱形继承问题, 比如当一个类同时继承了两个拥有相同基类的类,而最终的派生类又同时继承了这两个类时, 可能导致二义性和代码设计上的复杂性。为了解决这些问题,C++ 提供了虚继承, 通过在继承声明中使用 virtual 关键字,可以避免在派生类中生成多个基类的实例,从而解决了菱形继承带来的二义性。
#include <iostream>
class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
class Mammal : public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};
class Bird : public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};
// 菱形继承,同时从 Mammal 和 Bird 继承
class Bat : public Mammal, public Bird {
public:
void navigate() {
// 这⾥可能会引起⼆义性,因为 Bat 继承了两个 Animal
// navigate ⽅法中尝试调⽤ eat ⽅法,但不明确应该调⽤ Animal 的哪⼀个实现
eat();
}
};
int main() {
Bat bat;
bat.navigate();
return 0;
}
虚继承:
#include <iostream>
class Animal {
public:
void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
class Mammal : virtual public Animal {
public:
void breathe() {
std::cout << "Mammal is breathing." << std::endl;
}
};
class Bird : virtual public Animal {
public:
void fly() {
std::cout << "Bird is flying." << std::endl;
}
};
class Bat : public Mammal, public Bird {
public:
void navigate() {
// 不再存在二义性,eat ⽅法来自于共享的 Animal 基类
eat();
}
};
int main() {
Bat bat;
bat.navigate();
return 0;
}
虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,如上图所示。如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针。
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
class Base {
public:
virtual void print() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void print() override {
cout << "Derived class" << endl;
}
};
使用多态是为了避免在父类里大量重载引起代码臃肿且难于维护。
重载与重写的区别:
重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体
补充,当子类实现了和父类相同名字的函数(参数列表可以不同),且不是虚函数,这种行为叫隐藏,可以通过指定作用域来调用不同函数
B b;
b.fun(2); //调用的是B中的fun函数
b.A::fun(2);
并且基类指针指向派生类对象时,只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。
C++中的多态性是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针或引用上调用派生类对象的函数,以便在运行时选择正确的函数实现。
class Shape {
public:
virtual void draw() const {
// 基类的默认实现
}
};
class Circle : public Shape {
public:
void draw() const override {
// 派⽣类的实现
}
};
Shape* shapePtr = new Circle();
shapePtr->draw(); // 调⽤的是 Circle 类的 draw() 函数
class MyClass {
public:
int memberVariable; // 成员变量的声明
void memberFunction() {
// 成员函数的实现
}
};
class MyClass {
public:
static int staticMemberVariable; // 静态成员变量的声明
static void staticMemberFunction() {
// 静态成员函数的实现
}
};
int MyClass::staticMemberVariable = 0; // 静态成员变量的定义和初始化
class MyClass {
public:
// 默认构造函数
MyClass() {
// 初始化操作
}
};
class MyClass {
public:
// 带参数的构造函数
MyClass(int value) {
// 根据参数进⾏初始化操作
}
};
class MyClass {
public:
// 拷⻉构造函数
MyClass(const MyClass &other) {
// 进⾏深拷贝或浅拷贝,根据实际情况
}
};
发生拷贝构造的情况:
class MyClass {
public:
// 委托构造函数
MyClass() : MyClass(42) {
// 委托给带参数的构造函数
}
MyClass(int value) {
// 进⾏初始化操作
}
};
移动构造函数(move和右值引用)
拷贝构造函数中,对于指针,我们采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。
浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。
所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;
转换构造函数:形参是其他类型变量,且只有一个形参,用于将其他类型的变量,隐式转换为本类对象
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
③ 类类型的成员对象的构造函数(按照初始化顺序)
④ 派生类自己的构造函数
C++中的虚函数的作用主要是实现了多态的机制。虚函数允许在派生类中重新定义基类中定义的函数,使得通过基类指针或引用调用的函数在运行时根据实际对象类型来确定。这样的机制被称为动态绑定或运行时多态。
在基类中,通过在函数声明前面加上 virtual 关键字,可以将其声明为虚函数。派生类可以重新定义虚函数,如果派生类不重新定义,则会使用基类中的实现。(注意缺省参数和virtual函数一起使用的时候一定要谨慎)
class Base {
public:
virtual void virtualFunction() {
// 虚函数的实现
}
};
class Derived : public Base {
public:
void virtualFunction() override {
// 派⽣类中对虚函数的重新定义
}
};
虚函数的实现通常依赖于一个被称为虚函数表(虚表)的数据结构。虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
每个类(包括抽象类)都有一个虚表,其中包含了该类的虚函数的地址。每个对象都包含一个指向其类的虚表的指针,这个指针被称为虚指针(vptr)。
当调用一个虚函数时,编译器会使用对象的虚指针查找虚表,并通过虚表中的函数地址来执行相应的虚函数。这就是为什么在运行时可以根据实际对象类型来确定调用哪个函数的原因。
C++中虚函数表位于只读数据段,也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
class Base {
public:
// 虚函数有实现
virtual void virtualFunction() {
// 具体实现
}
};
class AbstractBase {
public:
// 纯虚函数,没有具体实现
virtual void pureVirtualFunction() = 0;
// 普通成员函数可以有具体实现
void commonFunction() {
// 具体实现
}
};
抽象类是不能被实例化的类,它存在的主要目的是为了提供一个接口,供派生类继承和实现。抽象类中可以包含普通的成员函数、数据成员和构造函数,但它必须包含至少一个纯虚函数。即在声明中使用 virtual 关键字并赋予函数一个 = 0 的纯虚函数。
纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。通过在函数声明的末尾使用 = 0 ,可以将虚函数声明为纯虚函数。派生类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类。
class AbstractShape {
public:
// 纯虚函数,提供接⼝
virtual void draw() const = 0;
// 普通成员函数
void commonFunction() {
// 具体实现
}
};
虚析构函数是一个带有 virtual 关键字的析构函数。 主要作用是确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放对象所占用的资源。
通常,如果一个类可能被继承,且在其派生类中有可能使用 delete 运算符来删除通过基类指针指向的对象,那么该基类的析构函数应该声明为虚析构函数。
class Base {
public:
// 虚析构函数
virtual ~Base() {
// 基类析构函数的实现
}
};
class Derived : public Base {
public:
// 派⽣类析构函数,可以覆盖基类的虚析构函数
~Derived() override {
// 派⽣类析构函数的实现
}
};
虚析构函数允许在运行时根据对象的实际类型调用正确的析构函数,从而实现多态性。
如果基类的析构函数不是虚的,当通过基类指针删除指向派生类对象的对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类的资源未被正确释放,造成内存泄漏。
构造函数在对象的创建阶段被调用,对象的类型在构造函数中已经确定。因此,构造函数调用不涉及多态性,也就是说,在对象的构造期间无法实现动态绑定。虚构造函数没有意义,因为对象的类型在构造过程中就已经确定,不需要动态地选择构造函数。
class Base {
public:
// 错误!不能声明虚构造函数
virtual Base() {
// 虚构造函数的实现
}
virtual ~Base() {
// 基类析构函数的实现
}
};
常见的不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
主要区别在于如何处理对象内部的动态分配的资源。
class DeepCopyExample {
public:
int *data;
DeepCopyExample(const DeepCopyExample &other) {
// ⼿动分配内存并复制数据
data = new int(*(other.data));
}
~DeepCopyExample() {
// 释放动态分配的资源
delete data;
}
DeepCopyExample& operator=(const DeepCopyExample &other) {
// 复制数据
if (this != &other) {
delete data;
data = new int(*(other.data));
}
return *this;
}
};
class ShallowCopyExample {
public:
int *data;
// 使⽤默认拷⻉构造函数和赋值操作符
};
重载运算符函数,本质还是函数调用,所以重载后:
可以是和调用运算符的方式调用,data1+data2
也可以是调用函数的方式,operator+(data1, data2),这就要注意运算符函数的名字是“operator运算符”
在可以重载的运算符里有逗号、取地址、逻辑与、逻辑或
不建议重载:
逗号、取地址,本身就对类类型有特殊定义;逻辑与、逻辑或,有短路求值属性;逗号、逻辑与、或,定义了求值顺序。
运算符重载应该是作为类的成员函数or非成员函数
注意:
重载运算符,它本身是几元就有几个参数,对于二元的,第一个参数对应左侧运算对象,第二个参数对应右侧运算对象。而类的成员函数的第一个参数隐式绑定了this指针,所以重载运算符如果是类的成员函数,左侧运算对象就相当于固定了是this。
⼀些规则:
函数调用运算符:
lambda是函数对象。编译器是将lambda表达式翻译为一个未命名类的未命名对象,‘[’捕获列表‘]’(参数列表){函数体} 对应类中重载调用运算符的参数列表、函数体,捕获列表的内容就对应类中的数据成员。所以捕获列表,值传递时,要拷贝并初始化那些数据成员,引用传递就是直接用。
有两种初始化方式:
赋值初始化是在构造函数当中做赋值的操作,而列表初始化是做纯粹的初始化操作。
而赋值初始化对于一些成员类,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作,降低程序的效率
必须使用列表初始化的四种情况
① 当初始化一个引用成员时;
② 当初始化一个常量成员时;
③ 当调用一个基类的构造函数,而它拥有一组参数时;
④ 当调用一个成员类的构造函数,而它拥有一组参数时;
静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
虚函数才具有动态绑定,引用和指针都可以实现动态绑定
一:继承
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
二:组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点:
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。
②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。
1) Empty(); // 缺省构造函数//
2) Empty( const Empty& ); // 拷贝构造函数//
3) ~Empty(); // 析构函数//
4) Empty& operator=( const Empty& ); // 赋值运算符//
this指针是类的指针,指向对象的首地址。
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。
this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。
一个对象的this指针并不是对象本身的一部分,不会影响 sizeof(对象) 的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针),编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行
一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;
另外一种情况是当形参数与成员变量名相同时用于区分,如this->n = n (不能写成n = n)
(1)this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数为T * const this
class A{public: int func(int p){}};
//其中,func的原型在编译器看来应该是:
int func(A * const this,int p);
(2)由此可见,this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:
A a;a.func(10);//此处,编译器将会编译成:A::func(&a,10);
和静态函数的区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高
当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是this指针。
即使你并没有写this指针,编译器在链接时也会加上this的,对各成员的访问都是通过this的。
例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看this指针来查看具体是哪个对象在调用。
This指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。
当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能
够正常运行。一旦涉及到this指针,如操作数据成员,调⽤虚函数等,就会出现不可预期的问题。
delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
class A {};
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}
空类的大小是1, 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。
具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。当该空白类作为基类时,该类的大小就优化为0了,子类的大小就是子类本身的大小。这就是所谓的空白基类最优化。
空类的实例大小就是类的大小,所以sizeof(a)=1字节,如果a是指针,则sizeof(a)就是指针的大小,即4字节。
class A { virtual Fun(){} };
int main(){
cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器);
A a;
cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器);
return 0;
}
因为有虚函数的类对象中都有一个虚函数表指针 __vptr,其大小是4字节
class A { static int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}
静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类的大小
class A { int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}
class A { static int a; int b; };;
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}
静态成员a不占用类的大小,所以类的大小就是b变量的大小,即4个字节
这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。
广义上讲,STL分为3类:Algorithm(算法)、Container(容器)、Iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。
详细的说,STL由6部分组成,即六大组件::容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器 。彼此之间可以组合套用、
STL 具有高可重用性,高性能,高移植性,跨平台的优点。
1.高可重用性:
STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
2.高性能:
如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
3.高移植性:
如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。
STL 的⼀个重要特性是将数据和操作分离
数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作。
保存两个数据成员,用来生成特定类型的模板。
使用: pair<T1, T2>p;
内部定义
namespace std {
template <typename T1, typename T2>
struct pair {
T1 first;
T2 second;
};
}
pair在底层被定义为⼀个struct,其所有成员默认是public,两个成员分别是first和second
其中map的元素是pair,pair<const key_type,mapped_type>,可以用来遍历关联容器
map<string,int>p;
auto map1 = p.cbegin();
while(map1 != p.cend())
{
cout<<map1->first<<map1->second<<endl;
++map1;
}
对map进行插入,元素类型是pair:
p.insert({word, 1});
p.insert(pair<string, int>(word, 1));
insert对不包含重复关键字的容器,插入成功返回pair<迭代器,bool>迭代器指向给定关键字元素,bool指出插入是否成功。
vector<pair<char, int>>result(val.begin(), val.end());
sort(result.begin(), result.end(),[](auto &a, auto &b){
return a.second > b.second;
});
template <class T, class Allocator = std::allocator<T>>
class vector {
private:
T* elements; // 指向数组起始位置的指针
size_t size; // 当前元素数量
size_t capacity; // 当前分配的内存块容量
};
有三个迭代器
(1)first : 指向的是vector中对象的起始字节位置
(2)last : 指向当前最后一个元素的末尾字节
(3)end : 指向整个vector容器所占用内存空间的末尾字节
(1)last - first : 表示 vector 容器中目前已被使用的内存空间
(2)end - last : 表示 vector 容器目前空闲的内存空间
(3)end - first : 表示 vector 容器的容量
2. 扩容过程
如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素
所以对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了
(1)堆中分配内存,元素连续存放,内存空间只会增长不会减少
vector有两个函数,一个是capacity(),在不分配新内存下最多可以保存的元素个数,另一个size(),返回当前已经存储数据的个数
(2)对于vector来说,capacity是永远大于等于size的,capacity和size相等时,vector就会扩容,capacity变大(翻倍)
这里涉及到了vector扩容方式的选择,新增的容量选择多少才适宜呢?
1、固定扩容
机制:每次扩容的时候在原 capacity 的基础上加上固定的容量,比如初始 capacity 为100,扩容一次为 capacity + 20,再扩容仍然为 capacity + 20;
缺点:考虑一种极端的情况,vector每次添加的元素数量刚好等于每次扩容固定增加的容量 + 1,就会造成一种情况,每添加一次元素就需要扩容一次,扩容的时间花费十分高昂。所以固定扩容可能会面临多次扩容的情况,时间复杂度较高;
优点:固定扩容方式空间利用率比较高。
2、加倍扩容
机制:每次扩容的时候原 capacity 翻倍,比如初始capcity = 100, 扩容一次变为 200, 再扩容变为 400;
优点:一次扩容 capacity 翻倍的方式使得正常情况下添加元素需要扩容的次数大大减少(预留空间较多),时间复杂度较低;
缺点:因为每次扩容空间翻倍,而很多空间没有利用上,空间利用率不如固定扩容。
在实际应用中,⼀般采用空间换时间的策略。
简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。
加倍扩容的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对内存不友好,最好把增长因子设为(1, 2),也就是1-2之间的某个数值。
对比可以发现采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。
3、resize()和reserve()
resize():改变当前容器内含有元素的数量(size()),而不是容器的容量
当resize(len)中len>v.capacity(),则数组中的size和capacity均设置为len;
当resize(len)中len<=v.capacity(),则数组中的size设置为len,而capacity不变;
reserve():改变当前容器的最大容量(capacity)
如果reserve(len)的值 > 当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前的对象通过copy construtor复制过来,销毁之前的内存;
当reserve(len)中len<=当前的capacity(),则数组中的capacity不变,size不变,即不对容器做任何改变。
3. vector迭代器:
由于vector维护的是一个线性区间,所以普通指针具备作为vector迭代器的所有条件,就不需要重载operator+,operator*之类的东西
4. vector的数据结构:线性空间。为了降低配置空间的成本,我们必须让其容量大于其大小。
template <class T>
struct Node {
T data;
Node* prev;
Node* next;
};
list是一个环状的双向链表,同时它也满足STL对于“前闭后开”的原则,即在链表尾端可以加上空白节点
支持快速随机访问,由于deque需要处理内部跳转,因此速度上没有vector快。
- | deque | vector |
---|---|---|
组织方式 | 按页或块来分配存储器的,每页包含固定数目的元素 | 分配一段连续的内存来存储内容 |
效率 | 即使在容器的前端也可以提供常数时间的insert和erase操作,而且在体积增长方面也比vector更具有效率 | 只是在序列的尾端插入元素时才有效率,但是随机访问速度要比deque快 |
栈与队列被称之为duque的配接器,其底层是以deque为底部架构。通过deque执行具体操作
heap(堆):
建立在完全二叉树上,分为两种,大根堆,小根堆,其在STL中做priority_queue的助手,即,以任何顺序将元素推入容器中,然后取出时一定是从优先权最高的元素开始取,完全二叉树具有这样的性质,适合做priority_queue的底层
priority_queue:
优先队列,也是配接器。其内的元素不是按照被推入的顺序排列,而是自动取元素的权值排列,缺省情况下利用一个max-heap完成,或者是以vector表现的完全二叉树。
map&&set
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
共同点: 都是C++的关联容器,只是通过它提供的接口对里面的元素进行访问,底层都是采用红黑树实现。
不同点:
set:用来判断某一个元素是不是在一个组里面。
map:映射,相当于字典,把一个值映射成另一个值,可以创建字典。
优点: 查找某一个数的时间为O(logn);遍历时采用iterator,效果不错。
缺点: 每次插入值的时候,都需要调整红黑树,效率有一定影响。
map && unordered_map的区别
这两者中元素是一些key-value对,关键字起索引作用,值表示和索引相关的数据。
底层实现:
map底层是基于红黑树实现的,因此map内部元素排列是有序的。
而unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。
map:
优点:有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。 map的查找、删除、增加等一系列操作时间复杂度稳定,都为O(logn )。
缺点:查找、删除、增加等操作平均时间复杂度较慢,与n相关。
unordered_map:
优点:查找、删除、添加的速度快,时间复杂度为常数级O(1)。
缺点:因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高。 unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(1),取决于哈希函数。极端情况下可能为O(n)。
问题:
为什么insert之后,以前保存的iterator不会失效:
因为 map 和 set 存储的是结点,不需要内存拷贝和内存移动。但是像 vector 在插入数据时如果内存不够会重新开辟一块内存。map 和 set 的 iterator 指向的是节点的指针,vector 指向的是内存的某个位置
为何map和set的插⼊删除效率比其他序列容器高:
因为 map 和 set 底部使用红黑树实现,插入和删除的时间复杂度是 O(logn),而 vector 这样的序列容器插入和删除的时间复杂度是 O(N)
container.push_back(value);
container 是一个支持 push_back 操作的容器,例如 std::vector 、 std::list 等,而 value 是要添加的元素的值。container.emplace_back(args);
其中 container 是一个支持 emplace_back 操作的容器,而 args 是传递给元素类型的构造函数的参数。与区别:
push_back 接受一个已存在的对象或一个可转换为容器元素类型的对象,并将其复制或移动到容器中。 emplace_back 直接在容器中构造元素,不需要创建临时对象。
emplace_back 通常比 push_back 更⾼效,因为它避免了创建和销毁临时对象的开销。
emplace_back 的参数是传递给元素类型的构造函数的参数,而 push_back 直接接受一个元素。
迭代器为不同类型的容器提供了统一的访问接口, 隐藏了底层容器的具体实现细节, 允许开发者使用一致的语法来操作不同类型的容器。
对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
a) string转const char*
string s = “abc”;
const char* c_s = s.c_str();
b) const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);
c) string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());
d) char* 转string
char* c = “abc”;
string s(c);
e) const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);
f) char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
traits技法利用“内嵌型别“的编程技巧与编译器的template参数推导功能,增强C++未能提供的关于型别认证方面的能力。常用的有iterator_traits和type_traits。
“trivial destructor”一般是指用户没有自定义析构函数,而由系统生成的,这种析构函数在《STL源码解析》中称为“无关痛痒”的析构函数。
反之,用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露
对于trivial destructor,如果每次都进行调用,显然对效率是一种伤害,如何进行判断呢?
《STL源码解析》中给出的说明是:
⾸先利⽤value_type()获取所指对象的型别,再利用__type_traits判断该型别的析构函数是否trivial,若是(__true_type),则什么也不做,若为(__false_type),则去调用destory()函数
也就是说,在实际的应用当中,STL库提供了相关的判断方法__type_traits,感兴趣的读者可以自行查阅使用方式。除了trivial destructor,还有trivial construct、trivial copy construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。
为什么需要二级空间配置器?
动态开辟内存时,要在堆上申请,但若是我们需要频繁的在堆开辟释放内存,则就会在堆上造成很多外部碎片,浪费了内存空间;
每次都要进行调用malloc、free函数等操作,使空间就会增加一些附加信息,降低了空间利用率;
随着外部碎片增多,内存分配器在找不到合适内存情况下需要合并空闲块,浪费了时间,大大降低了效率。
于是就设置了二级空间配置器,当开辟内存<=128bytes时,即视为开辟小块内存,则调用二级空间配置器。
关于STL中一级空间配置器和二级空间配置器的选择上,一般默认选择的为二级空间配置器。 如果大于128字节再转去一级配置器器。
一级空间配置器中重要的函数就是allocate、deallocate、reallocate 。 一级空间配置器是以malloc(),free(),realloc()等C函数执行实际的内存配置 。大致过程是:
1、直接allocate分配内存,其实就是malloc来分配内存,成功则直接返回,失败就调用处理函数
2、如果用户自定义了内存分配失败的处理函数就调用,没有的话就返回异常
3、如果自定义了处理函数就进行处理,完事再继续分配试试
记住前三个:
模板分为类模板与函数模板,特化分为特例化(全特化)和部分特例化(偏特化)。
对模板特例化是因为对特定类型,可以利用某些特定知识来提高效率,而不是使用通用模板。
对函数模板:
template<typename T1, typename T2>
void fun(T1 a, T2 b)
{
cout<<"模板函数"<<endl;
}
template<>
void fun<int , char >(int a, char b)
{
cout<<"全特化"<<endl;
}
函数模板,只有全特化,偏特化的功能可以通过函数的重载完成。
对类模板:
template<typename T1, typename T2>
class Test
{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
T1 a;
T2 b;
};
template<>
class Test<int , char>
{
public:
Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
int a;
char b;
};
template <typename T2>
class Test<char, T2>
{
public:
Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
char a;
T2 b;
}
对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类。
int func() {return 0};
//普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数
func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int
//不论是顶层const还是底层const, decltype都会保留
const int c = 3;
decltype(c) d = c; // d的类型和c是一样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const
//引⽤与指针类型
//1. 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&
//2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型
//3. 对指针的解引用操作返回的是引用类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在一起
//4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
参考-右值引用、移动语义和完美转发
左值右值:
左值: 可以放在等号左边,可以取地址并有名字
右值: 不可以放在等号左边,不能取地址,没有名字
字符串字面值"abcd"也是左值,不是右值
++i、–i是左值,i++、i–是右值
#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
cout << t << endl;
}
int getInt()
{
return 5;
}
int main() {
int a = 10;
int& b = a; //b是左值引⽤
int& c = 10; //错误,c是左值不能使⽤右值初始化
int&& d = 10; //正确,右值引⽤⽤右值初始化
int&& e = a; //错误,e是右值引⽤不能使⽤左值初始化
const int& f = a; //正确,左值常引⽤相当于是万能型,可以⽤左值或者右值初始化
const int& g = 10;//正确,左值常引⽤相当于是万能型,可以⽤左值或者右值初始化
const int&& h = 10; //正确,右值常引⽤
const int& aa = h;//正确
int& i = getInt(); //错误,i是左值引⽤不能使⽤临时变ᰁ(右值)初始化
int&& j = getInt(); //正确,函数返回值是右值
fun(10); //此时fun函数的参数t是右值
fun(a); //此时fun函数的参数t是左值
return 0;
}
nullptr是用来代替NULL,一般C++会把NULL、0视为同一种东西,这取决于编译器如何定义NULL,有的定义为((void*)0),有的定义为0
C++不允许直接将void* 隐式转换到其他类型,在进行C++重载时会发生混乱
例如:
void foo(char *);
void foo(int );
如果NULL被定义为 ((void*)0),那么当编译char *ch = NULL时,NULL被定义为 0
当foo(NULL)时,此时NULL为0,会去调用foo(int ),从而发生混乱
为解决这个问题,从而需要使用NULL时,用nullptr代替:
C++11引入nullptr关键字来区分空指针和0。nullptr 的类型为 nullptr_t,能够转换为任何指针或成员指针的类型,也可以进行相等或不等的比较。
基于范围的迭代写法,for(变量:对象)表达式
例如对vector中的元素进行遍历:
std::vector<int> arr(5, 100);
for (std::vector<int>::iterator i = arr.begin(); i != arr.end(); i ++) {
std::cout << *i << std::endl;
}
// 范围for循环
for (auto &i : arr) {
std::cout << i << std::endl;
}
C++定义了几种初始化方式,例如对一个int变量 x初始化为0:
int x = 0; // method1
int x = {0}; // method2
int x{0}; // method3
int x(0); // method4
采⽤花括号来进行初始化称为列表初始化,无论是初始化对象还是为对象赋新值。
⽤于对内置类型变量时,如果使用列表初始化,且初始值存在丢失信息风险时,编译器会报错。
long double d = 3.1415926536;
int a = {d}; //存在丢失信息⻛险,转换未执⾏。
int a = d; //确实丢失信息,转换执⾏。
lambda表达式表示一个可调用的代码单元,没有命名的内联函数,不需要函数名因为我们直接(一次性的)用它,不需要其他地方调用它。
每当你定义⼀个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。
[capture list] (parameter list) -> return type {function body }
// [捕获列表] (参数列表) -> 返回类型 {函数体 }
// 只有 [capture list] 捕获列表和 {function body } 函数体是必选的
auto lam =[]() -> int { cout << "Hello, World!"; return 88; };
auto ret = lam();
cout<<ret<<endl; // 输出88
-> int :代表此匿名函数返回int,大多数情况下lambda表达式的返回值可由编译器猜测得出,因此不需要我们指定返回值类型。
int a = 1, b = 2, c = 3;
auto lam2 = [&, a](){ //b,c以引⽤捕获, a以值捕获
b = 5;
c = 6; //a = 1,a不能赋值
cout << a << b << c << endl; //输出 1 5 6
};
lam2();
void fcn() { //值捕获
size_t v1 = 42;
auto f = [v1] {return v1;};
v1 = 0;
auto j = f(); //j = 42,创建时拷⻉,修改对lambda内对象⽆影响
}
void fcn() { //可变lambda
size_t v1 = 42;
auto f = [v1] () mutable {return ++v1;}; //修改值捕获可加mutable
v1 = 0;
auto j = f(); //j = 43
}
void fcn() { //引⽤捕获
size_t v1 = 42; //⾮const
auto f = [&v1] () {return ++v1;};
v1 = 0;
auto j = f(); //注意此时 j = 1
}
lambda最大的一个优势是在使用STL中的算法(algorithms)库
例如:数组排序
int arr[] = {6, 4, 3, 2, 1, 5};
bool compare(int& a, int& b) { //谓词函数
return a > b;
}
std::sort(arr, arr + 6, compare);
//lambda形式
std::sort(arr, arr + 6, [](const int& a, const int& b){return a > b;}); //降序
std::for_each(begin(arr), end(arr), [](const int& e){cout << "After:" << e << endl;});
//6, 5, 4, 3, 2, 1
default (1) | thread () noexcept; |
---|---|
initialization (2) | template <class Fn, class… Args> explicit thread (Fn&& fn, Args&&… args); |
copy [deleted] (3) | thread (const thread&) = delete; |
move (4) | thread (thread&& x) noexcept; |
1、默认构造函数,创建一个空的 thread 执行对象。
2、初始化构造函数,创建一个 thread对象,该 thread对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
3、拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
4、move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
注意:
可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.
std::thread在使用上容易出错,即std::thread对象在线程函数运行期间必须是有效的。什么意思呢?
#include <iostream>
#include <thread>
void threadproc() {
while(true) {
std::cout << "I am New Thread!" << std::endl;
}
}
void func() {
std::thread t(threadproc);
}
int main() {
func();
while(true) {} //让主线程不要退出
return 0;
}
以上代码再main函数中调用了func函数,在func函数中创建了一个线程,乍一看好像没有什么问题,但在实际运行时会崩溃。
崩溃的原因:
在func函数调用结束后,func中局部变量t(线程对象)被销毁,而此时线程函数仍在运行。所以在使用std::thread类时,必须保证线程函数运行期间其线程对象有效。
std::thread对象提供了一个detach方法,通过这个方法可以让线程对象与线程函数脱离关系,这样即使线程对象被销毁,也不影响线程函数的运行。
只需要在func函数中调用detach方法即可,代码如下:
// 其他代码保持不变
void func() {
std::thread t(threadproc);
t.detach();
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。