自学内容网 自学内容网

C++ 八股文整理(百度paddlepaddle一面C++相关问题

1. 生成可执行文件过程

C++八股文
C++八股文
C++八股文
```cpp
#include<stdio.h>

#define   MAX  20 
#define   MIN  10 

int main()
{
printf("this is a compile sample\n");
printf("MAX = %d,MIN = %d,MAX + MIN = %d\n",MAX,MIN,MAX + MIN); 

return 0;
}
```

1.1 预处理

  • 处理宏定义和include
  • #ifdef 和 #endif等等宏定义
  • 比如#include<stdio.h>告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中
  • INCLUDE(搜include中文件的路径)
    LIBPATH(搜使用using 引入文件的路径)
    LIB(搜库文件的路径
  • 去除注释
  • 为代码添加行号,便于debug
  • 不会对语法进行检查,生成.i文件
  • gcc -E test.c -o test.i

在这里插入图片描述

1.2 编译

  • 检查语法,生成汇编指令, .s文件。
  • gcc -s test.c -o test.s

在这里插入图片描述

1.3 汇编

  • 翻译成符合一定格式的机器代码,生成二进制文件。
  • 但是此时的二进制文件不可执行
  • gcc -c test.c -o test.o
    在这里插入图片描述在这里插入图片描述

1.4 链接

  • 将汇编生成的OBJ文件、系统库的OBJ文件、库文件链接起来,最终生成可以在特定平台运行的可执行程序。
  • gcc最终会调用ld命令
  • gcc -o test test.c
    在这里插入图片描述

2. 链接过程

链接过程
GOT和plt

2.1 静态链接

  • 由链接器在链接时将库的内容直接加入到可执行程序中
  • 编译静态库源码:gcc -c lib.c -o lib.o
  • 生成静态库文件:ar -q lib.a lib.o
  • 使用静态库编译:gcc main.c lib.a -o main.out

在这里插入图片描述

2.2 动态链接

  • 可执行程序在运行时才动态加载库进行链接
  • 库的内容不会进入可执行程序当中
  • 在运行过程中,删除动态库会导致运行失败,如果是静态库则不会

3. linux中的锁机制

在这里插入图片描述

3.1 mutex

在并发编程中,“mutex”(互斥量)是一种特定类型的锁。因此,"mutex"和"锁"这两个术语在某种程度上可以互换使用。然而,在某些上下文中,"锁"可能被用作一个更通用的术语,指代各种用于保护共享资源的同步机制,包括互斥锁、读写锁、自旋锁等。
Linux中的mutex底层原理是通过原子操作和自旋锁来实现临界区的互斥访问,确保多个线程在同一时刻只有一个线程可以进入临界区执行代码。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
  mtx.unlock();
}

int main ()
{
  std::thread th1 (print_block,50,'*');//线程1:打印*
  std::thread th2 (print_block,50,'$');//线程2:打印$

  th1.join();
  th2.join();

  return 0;
}

3.2 其他类型的锁

读写锁(Read-Write Lock)允许多个线程同时读取共享资源,但只有一个线程可以写入资源。自旋锁(Spin Lock)是一种忙等待的锁,线程在尝试获取锁时会循环执行检查,而不是被阻塞。

  • 互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它,互斥锁可适用于一个共享资源每次只能被一个线程访问的情况
  • 自旋锁适用于:短暂的访问临界区时适用 – 达到减少切换消耗 读写锁适用于:用于解决读操作较多的场景 – 减少锁等待提高并发性。

单核心多线程时,锁的实现可能依赖于关闭中断,因为线程切换是通过系统调用实现的,需要中断,如果关闭了中断则可以避免线程被切换

3.3 原子指令

有两个线程,一个要写数据,一个读数据,如果不加锁,可能会造成读写值混乱,使用std::mutex程序执行不会导致混乱,但是每一次循环都要加锁解锁是的程序开销很大。为了提高性能,C++11提供了原子类型(std::atomic),它提供了多线程间的原子操作,可以把原子操作理解成一种:不需要用到互斥量加锁(无锁)技术的多线程并发编程方式。它定义在头文件中,原子类型是封装了一个值的类型,它的访问保证不会导致数据的竞争,并且可以用于在不同的线程之间同步内存访问。从效率上来说,原子操作要比互斥量的方式效率要高。
.atomic底层采用的cas自旋,比较并交换,来保证的原子性 2.cas底层操作的是cpu指令,比较当前的值和主内存的值是否一致
锁也是使用的cas指令,但是锁需要进行系统调用,所以会比原子指令更慢
但我觉得原子指令没有办法锁住一片区域

3.4 乐观锁和悲观锁

前面提到的互斥锁、自旋锁、读写锁,都属于悲观锁。

悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

而乐观锁认为冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

乐观锁全程没有加锁,所以它也叫无锁编程。

乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

4. 程序内存分配

  • 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局区(静态区)(static)— 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放
  • 文字常量区 — 常量字符串就是放在这里的。 程序结束后由系统释放。程序代码区 — 存放函数体的二进制代码。

4.1 堆和栈的区别

1、分配方式不同

栈:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
堆:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)new(10);
但是注意p1、p2本身是在栈中的。

2、 空间大小不同

一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。

3、分配效率

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执 行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考 数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

4、碎片问题

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。

5、生长方向

对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

5. static

5.1 面向过程设计中的static

1、全局静态变量
• 变量在全局数据区分配内存;
• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

2、局部静态变量
• 变量在全局数据区分配内存;
• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

3、静态函数
• 静态函数不能被其它文件所用;
• 其它文件中可以定义相同名字的函数,不会发生冲突;

5.2 面向对象的static关键字(类中的static关键字)

1、类的静态数据成员
• 静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。语句int Myclass::Sum=0;是定义静态数据成员;
• 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
• 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
• 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
<数据类型><类名>::<静态数据成员名>=<值>
• 类的静态数据成员有两种访问形式:
<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
• 静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;

2、类的静态成员函数
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
• 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
<类名>::<静态成员函数名>(<参数表>)
调用类的静态成员函数。

5.3 区别

全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。

局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

从分配内存空间看:
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。

从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。

static全局变量与普通的全局变量有什么区别:
static全局变量只初使化一次,防止在其他文件单元中被引用;   
static局部变量和普通局部变量有什么区别:
static局部变量只被初始化一次,下一次依据上一次结果值;   
static函数与普通函数有什么区别:
static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

6. class vs struct vs union

6.1 union

union 共用体(联合体)
在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中 以关键字union声明的一种数据结构,这种被称作“共用体”类型结构,也叫联合体。

“联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间,一个结构体变量的总长度大于等于各成员长度之和。而在“联合”中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。注意这里所谓的共享不是指把多个成员同时装入一个联合变量内,而是指该联合变量可被赋予任一成员值,但每次只能赋一种值,赋入新值则冲去旧值,共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员就失去作用,共用体变量的地址和它的各成员的地址都是同一地址

union可以用于大端、小端模式确定

6.2 struct

在struct中,默认的成员访问权限是public。这意味着,在结构体外部,我们可以直接访问其成员变量和成员函数。
当涉及到继承时,struct和class之间的另一个区别是它们的默认访问级别。在C++中,类可以从其他类或结构体继承,反之亦然。在struct中,继承默认访问权限是public。
在C++中,对象的大小是很重要的,它决定了对象在内存中的大小。在struct和class中,对象的大小也有一些区别。
在struct中,对象的大小等于所有成员变量的大小之和
这里说的struct更多的是C++的struct,所以可以有函数的概念,但是C里面的struct是没有的

6.3 class

与struct不同,class的默认成员访问权限是private。这意味着,我们不能在类的外部直接访问其成员变量和成员函数。为了访问这些成员,我们需要使用public成员函数。
而在class中,继承默认访问权限是private。
在class中,对象的大小还要考虑虚函数表的大小。
虚函数表是一张表格,用于存储类中的虚函数指针。每个含有虚函数的类都有一个虚函数表。当对象被实例化时,它会分配一段内存来存储虚函数表。虚函数表的大小取决于类中的虚函数数量和编译器实现。

6.4 字节对齐

为了更高的效率。
32bit的系统,每次可以读取32bit,即4个字节,只要一个四字节长度的变量放在这四字节对齐内,就能一次读完(例如放在0x04到0x07内,则系统能一次性读完);
如果这个变量放在0x05到0x08内,那么,系统第一次只能是先读4-7(取5-7的内容),第二次再读8-11(取8的内容),然后,还得把他们组合在一起才行。
这么看来,速率不就是差了很多了?

字节对齐的隐患:
很多时候不需要考虑,但是,如果对于某些本身没有4对齐的结构体,但是,却要根据某字节的长度进行跳转读取的(使用sizeof),则会出现问题。
例如是在读取bmp图片的文件头时(假设存放地址为0x00),它的结构体长度为14(实际),但是,默认情况下使用sizeof的时候,它就是16了,读取的指针一下子就窜到了0x17,便遗漏了0x15,0x16的内容,同时,0x17后面的内容也全部乱套了。。。。
因此,此时,则需要取消字节对齐,取消方法下面有。

为什么bool是一个字节
1.效率:CPU 一下子就能处理 32bit or 64bit的数据,所以直接填满了来处理,这样可能效率更高。比如说,有的目标平台处理双字节的速度比单字节要快,于是很多单字节数据类型就可以用自定义一个双字节类型代替。bool亦如是,只不过是把自定义(成单字节数据)这件事转给编译器做了而已。

2.寻址:如果要是有只占用一个 bit 的变量,那么它保存的时候也应该是只占用一个 bit,假设这一个 bit 放在一个空的byte 的首位,那么剩下的七个 bit要怎么处理呢?存东西还是不存东西?如果要存,那么之后的顺序就都乱了------地址值要具体到每一个 byte 了,8 =2^3,所以地址值直接增加三位,这样整个系统的代价太大了。所以就直接用 byte来保存,浪费点是难免的,要以大局为重。(类似于结构体的字节对齐现象)。

7. 类

7.1 类的访问属性:public,protect,private

C++中类的成员变量和函数都带有三种属性中的一种,假如没有特别声明,那么就默认是私有的(除了构造函数)。public表示是公开的,对象可以直接调用的变量或者函数;protect表示是保护性的,只有本类和子类函数能够访问(注意只是访问,本类对象和子类对象都不可以直接调用),而私有变量和函数是只有在本类中能够访问(有个例外就是友元函数,这个后面会详细说)。
而子类对父类的继承类型也有这三种属性,分别为公开继承,保护继承和私有继承。
public: protected: private:
public继承 public protected 不可用
protected继承 protected protected 不可用
private继承 private private 不可用

7.2 构造函数

class A
{
public:
    A(int i):m_a(i){}
    int m_a;
};
int main( void )
{
    A a; //错误!没有无参构造函数
    A a1(5); // 调用了A中程序员定义的有参构造函数
    A a2(6); // 调用了A中程序员定义的有参构造函数
    A a3 = a1; //此处调用默认的拷贝构造函数
    a2 = a1; //此处调用默认的赋值函数
}

默认的赋值和拷贝构造函数一般只是简单的拷贝类中成员的值,这一点当类中存在指针成员和静态成员变量的时候就非常危险。

子类会继承父类所有的函数,包括构造函数,但是子类的构造函数会把父类的构造函数覆盖了,所以看起来就是没有继承。假如子类不定义任何构造函数,那么子类只会默认地调用父类的无参构造函数。当父类中只定义了有参构造函数,从而不存在无参构造函数的话,子类就无法创建对象。

假如把构造函数定义为私有,那么类就无法直接实例化(还是可以实例化的,只是要转个弯)。来看下面这个例子:

class A
{
public:
    int m_public;
    static A* getInstance(int num)
    {
        return new A(num);
    }
private:
    A(int b):m_public(b){}
    
};

int main()
{
    A a1(4); //错误
    A* pa = A::getInstance(5); //正确
}

7.3 深拷贝、浅拷贝

1、浅拷贝,指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。
2、深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。

3、区别:可变对象就不会这样会修改值后另存到一个新的地址上,而是直接再原对象的地址上把值给改变了,这个对象依然执行这个地址

4、本质区别:可变对象修改了值,不会新建一个内存地址的对象,不可变对象如果修改了值,及时复制了一份新的内存地址,原始地址的值不会被改变。

5、不可变元素包含:int,float,complex,long,str,unicode,tuple

6、可变原生:list

7.4 虚函数

虚函数是C++实现多态的方法。 虚函数和普通函数没有什么区别,只有当用基类指针调用子类对象的方法时才能够真正发挥它的作用,也只有在这种情况下,才能真正体现出C++面对对象编程的多态性质。

先来了解一下绑定的概念。函数体与函数调用关联起来叫做绑定。

早绑定:早绑定发送在程序运行之前,也是编译和链接阶段。
在上面的代码中,函数add在编译期间就已经确定了实现。这就是早绑定。所有的非虚函数都是早绑定。

晚绑定:晚绑定发生在程序运行期间,主要体现在继承的多态方面。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚邦定,它就不是多态。”

8. new 和 malloc

new和malloc的区别以及底层实现原理
malloc底层实现原理

在Linux环境下

当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)

brk 是将数据段(.data)的最高地址指针 _edata 往高地址推
mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

malloc基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会用sbrk()才推进brk指针来申请内存空间。

brk

进程调用A=malloc(30K)以后,malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配,难道这样就完成内存分配了?

事实是:_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
mmap

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

这样子做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放.
new底层实现原理

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。

new在底层调用operator new函数申请空间,delete在底层通过operator delete函数释放空间。

operator new实际通过malloc申请空间,申请成功直接返回,申请失败就尝试空间不足的对应措施set_new_hander,如果用户设置了这个措施就继续申请,否则抛出异常

简单类型直接调用operator new分配内存;
可以通过new_handler来处理new失败的情况;
new分配失败的时候不像malloc那样返回NULL,它直接抛出异常。要判断是否分配成功应该用异常捕获的机制;
new 复杂数据类型(需要由构造函数初始化对象)的时候先调用operator new,然后在分配的内存上调用构造函数。

9. C++11 特性

9.1auto 和 decltype

C++11中用auto关键字来支持自动类型推导。用decltype推导表达式类型。头文件:#include
auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。(前提是定义一个变量时对其进行初始化)

auto a = 10; // 10是int型,可以自动推导出a是int
decltype:用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。(decltypde是不需要推导变量初始化的,根据的是表达式对变量的类型就可以推导。)

auto varname = value;
decltype(exp) varname = value;
decltype(10.8) x; //x 被推导成了 double
两者区别:

1,auto用于变量的类型推导,根据初始化表达式的类型来推导变量的类型,常用于简化代码和处理复杂类型。而decltype则用于获取表达式的类型,保留修饰符,并且可以进行表达式求值。

2,auto在初始化时进行类型推导,而decltype直接查询表达式的类型,可以用于任何表达式,包括没有初始化的变量。

3,auto在编译期间确定类型,并且无法更改。而decltype在运行时才确定表达式的类型。

4,auto适用于简单的类型推导,而decltype适用于复杂的类型推导和获取表达式的结果类型。

9.2 范围 for

在C++11中,引入了范围for循环(Range-based for loop),它提供了一种简洁而直观的方式来遍历容器、数组、字符串和其他可迭代对象。

for (auto element : container) {
// 操作每个元素
}
其中,element 是一个变量,用于存储容器中的每个元素的值。container 是一个可迭代对象,例如数组、标准库容器或自定义容器。

范围for循环的工作原理是,它会自动遍历容器中的每个元素,并将当前元素的值赋给 element 变量,然后执行循环体中的代码块。循环体会针对容器中的每个元素执行一次。

//实例:
#include
#include

int main() {
std::vector numbers = {1, 2, 3, 4, 5};

for (auto number : numbers) {
    std::cout << number << " ";
}

return 0;

}

9.3 智能指针

在C++11中,引入了新的智能指针类,用于更安全和方便地管理动态分配的资源,避免内存泄漏和悬空指针等问题。以下是C++11中的三种主要智能指针:

【1】std::unique_ptr:

1,std::unique_ptr 是一种独占式智能指针,用于管理唯一的对象,确保只有一个指针可以访问该对象。

2,使用 std::unique_ptr 可以自动释放动态分配的内存,当指针超出作用域或被重置时,它会自动删除所管理的对象。

3,通过 std::make_unique 函数可以创建 std::unique_ptr 对象,如:std::unique_ptr ptr = std::make_unique(42);

在这里插入图片描述

【2】std: :shared_ptr:

1,std::shared_ptr 是一种共享式智能指针,多个指针可以同时共享对同一对象的拥有权。

2,std::shared_ptr 使用引用计数技术追踪所管理对象的引用数量,当引用计数变为零时,自动销毁所管理的对象。

3,通过 std::make_shared 函数可以创建 std::shared_ptr 对象,如:std::shared_ptr ptr = std::make_shared(42);

【3】std::weak_ptr:

1,std::weak_ptr 是一种弱引用智能指针,它可以解决 std::shared_ptr 的循环引用问题。

2,std::weak_ptr 指向 std::shared_ptr 管理的对象,但不会增加引用计数。因此,当所有 std::shared_ptr 对象超出作用域后,即使还有 std::weak_ptr 对象存在,所管理的对象也会被销毁。

3,通过 std::shared_ptr 的 std::weak_ptr 构造函数可以创建 std::weak_ptr 对象,如:std::weak_ptr weakPtr = sharedPtr;

注意:“它们的确智能,但它们仍然是指针。”除非我们确实需要指针,否则,简单地使用局部变量会更好

在这里插入图片描述

9.4 nullptr

nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针

9.5 explicit

explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:

struct A {
explicit A(int value) {
cout << “value” << endl;
}
};

int main() {
A a = 1; // error,不可以隐式转换
A aa(2); // ok
return 0;
}

9.6final & override

c++11关于继承新增了两个关键字,final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载,override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。

9.7 lambda表达式

9.8 左值和右值

左值和右值
左值和右值
左值引用
1.1 左值
在以前,大家都应该学过引用,在C++中,引用还是比较常用的,因为它是给变量取别名,可以减少一层拷贝。但是,在C++11增加了右值引用后,我们以前所使用的引用都应该叫做“左值引用”。

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,也可以对它赋值。左值一般出现在符号的左边,但也可能出现做符号的右边。const修饰的左值,不能给他赋值,但是可以取它的地址。而左值引用,其实就是给左值取别名。

在以前,大家可能听说过“在赋值符号左边的就是左值”,或者“不能修改的就是左值”。这两种说法其实都是错误的。例如将一个变量的赋值给另一个变量时,该表达式的左右两个值都是左值;const修饰的左值,虽然也是左值,但是它却不允许修改。

如下图中的所有变量,其实都是左值。其中的“int* p1 = &d;”中的p1和d其实都是左值。

1.2 右值
右值也是一个数据的表达式。如字面常量、表达式返回值、函数返回值(不能是左值引用返回)等待。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边。右值不能取地址。右值引用是对右值的引用,给右值取别名。

当然,不能简单的将右值理解为“在赋值符号右边的值”或“不能修改的值就是右值”。这两种理解都是错误的。例如在上面的左值的图中,就有在赋值符号右边,但是是左值的值;也有被const修饰无法被修改,但是是左值的值。

如下图中的都是一些常见的右值:

1.3 左值与右值的区分
要区分左值与右值,只需要记住一个标准:“左值可以取地址,右值不能被取地址”。这就是左值和右值的本质区别。
因为左值一般是被放在地址空间中的,有明确的存储地址;而右值一般可能是计算中产生的中间值,也可能是被保存在寄存器上的一些值,总的来讲就是,右值并没有被保存在地址空间中,也就无法取地址。

  1. 左值引用与右值引用
    2.1 左值引用与右值引用的使用方法
    左值引用的方法很简单,就是大家以前所学习的引用,即在变量类型的右边加上一个“&”即可:

右值引用的使用也很简单,在变量类型的右边加上两个“&&”即可:

首先要提出一个新的概念: 将亡值(xvalue,expiring value),是C++11为了引入右值引用而提出的概念(因此在传统 C++中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

C++11 引入了右值引用的概念,以表示一个本应没有名称的临时对象。右值引用的声明与左值引用类似,但是它使用的是 2 个 & 符号(&&),以下代码使用了右值引用打印了两次 5 的平方:

int && rRef = square(5);
cout << rRef << endl;
cout << rRef << endl;
有意思的是,声明一个右值引用,给一个临时内存位置分配一个名称,这使得程序的其他部分访问该内存位置成为了可能,并且可以将这个临时位置变成一个左值。

右值引用不能约束到左值上,所以,以下代码将无法编译:

int x = 0;
int && rRefX = x;
再来看以下初始化语句:

int && rRef1 = square(5);
在初始化完成之后,这个包含值 square(5) 的内存位置有了一个名称,即 rRef1,所以 rRef1 本身变成了一个左值。这意味着后面的这个初始化语句将不会编译:

int && rRef2 = rRef1;
究其原因,就是右侧的 rRef1 不再是一个右值。综上所述,临时对象最多可以有一个左值引用指向它。如果函数有一个临时对象的左值引用,则可以确认,程序的其他部分都不能访问相同的对象。

右值引用会涉及到很多概念比如移动语义,和move一起使用更合适


原文地址:https://blog.csdn.net/ugnbucbjj/article/details/137815789

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!