c++复习

------------------ 基础-------------------

内存分区

栈: 存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。

堆: 动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。

全局区/静态存储区(.bss 段和 .data 段): 存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

常量存储区(.data 段): 存放的是常量,不允许修改,程序运行结束自动释放。

代码区(.text 段): 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

第二种分区:

内存分区模型
代码区:存放函数的二级制代码,由操作系统进行管理的
全局区:存放全局变量和静态变量以及常量
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
堆区: 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:不同区域存放的数据,赋予不同的声明周期,给我们更大的灵活编程


————————————————
原文链接:https://blog.csdn.net/qq_51604330/article/details/118607922

程序编译四个阶段

 预处理阶段

1.什么是预处理
        C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。

预处理的作用
        在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。预编译的主要作用如下:
●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。

预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。

编译阶段

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。

汇编阶段

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符,如果我们在文本文件中打开hello.o文件,看到的将是一堆乱码。

链接阶段

链接器(ld)负责处理合并目标代码

将主程序以及需要调用的库整合在一起,生成一个可执行目标文件,可以被加载到内存中,由系统执行。

其中,库分为动态库和静态库,他们的介绍和使用可以参考这篇文章:http://t.csdnimg.cn/GUdrx

原码,反码, 补码

原码即十进制数的二进制形式,在最高位加上一个符号位1,0代表正数,1代表负数

若某数为正数或者0,则原码=反码=补码

若某数为负数,反码等于原码除符号位之外每位取反,补码等于反码+1

引入补码的原因为解决计算机通过原码进行2+(-2)这类运算会出错的问题,在计算机中只有设计了加法运算器,所以若需要进行减法运算入2-2,则只能利用加法2+(-2)进行运算,而利用原码进行带有负数的加法运算会出错,于是引出了补码。

总结:在计算机中,存储,运算时的数据形式都为补码,输出形式为原码

浮点数

M x 2^E  M为尾数,E为阶数,2为基数,计算机领域默认基数为2

C中的float和double数据类型采用IEEE 754 规则

float,共 4个字节 32位,第1位最高位为符号位,8位指数位,23位尾数位

float 类型的有效数字位数为大约 7 位

double,共 8个字节 64位,第1位最高位为符号位,11位指数位,52位尾数位

double 类型的有效数字位数为 15-16 位

C语言中整型数据、浮点型数据在内存中的存储(超详细)_c语言 32位浮点型 整型-CSDN博客

左值和右值的区别

  • 左值是可寻址的变量,有持久性;
  • 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。

左值(lvalue)和右值(rvalue)最先来源于编译。在C语言中表示位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。
定义:
左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以作为一个左值。
右值指的是引用了一个存储在某个内存地址里的数据。
从上面的两个定义可以看出,左值其实要引用一个对象,而一个对象在我们的程序中又肯定有一个名字或者可以通过一个名字访问到,所以左值又可以归纳为:左值表示程序中必须有一个特定的名字引用到这个值。而右值引用的是地址里的内容,所以右值又可以归纳为:右值表示程序中没有一个特定的名字引用到这个值。

++a的话因为返回结果和运算之后的a一样,所以++a返回的是真实的a,可以被重新赋值,所以可以作为左值。而a++返回的是运算之前的a,而此时a已经+1了,返回的数据其实是过去的a,它是另外复制出来的,而不是真正的a,所以无法被赋值,所以它只能是右值。

所以a++;在执行当中的顺序是,先把a的值复制出来,进行整体运算,然后再a=a+1。
————————————————
原文链接:https://blog.csdn.net/qq_33148269/article/details/78046207

结构体字节对齐

结构体字节对齐详解【含实例】-CSDN博客

类型转换

c语言中

一种为隐式类型转换

另一种为强制类型转换

强制类型转换的一般形式为:
(类型名)表达式
例如:

int a=7,b=2;
float y1,y2;
float y1=a/b;/*y1的值a/b为3.0*/
y2=(float)a/b;/*y2的值为3.5,float将a进行强制转换为实型,b也随之自动转换为实型*/

c++中

下面是四种C++中的类型转换方式:

  1. 常量类型转换(const_cast ):

    • 该运算符用来修改引用和指针数据类型的const属性,常量指针被转化成非常量指针,并且仍然指向原来的对象 常量引用被转换成非常量引用,并且仍然指向原来的对象
    • 不能直接对非指针和非引用的变量使用 const_cast 操作符
  2. 动态类型转换(dybamic_cast):
    • 具有类型检查的功能,比 static_cast 更安全
  3. 静态转换(Static Cast):

    • 在编译期进行基本的类型转换,类似于C语言中的显式类型转换。
    • 主要用于基本数据类型之间的转换,指针和引用之间的转换,以及父子类之间的指针或引用转换。
    • 没有类型安全检查,是不安全的,转换的安全性也要开发人员来保证
    • 使用 static_cast<type>(expression) 进行转换。
  4. 重新解释转换(Reinterpret Cast)

    • 用于在不同类型之间进行低级别的转换,通常是将指针或引用从一种类型转换为另一种不同的类型。
    • 重新解释转换是一种较为危险的转换,因为它会忽略类型之间的任何类型安全检查,仅仅是对比特进行重新解释。
    • 使用 reinterpret_cast<type>(expression) 进行转换。

异常捕获

为什么存在异常处理
在程序运行时常会碰到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。

而 C++ 异常处理机制就可以让我们捕获并处理这些错误,然后我们可以让程序沿着一条不会出错的路径继续执行,或者不得不结束程序,但在结束前可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。

程序的错误的三种分类
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:

语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。

逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。

运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++ 异常(Exception)机制就是为解决运行时错误而引入的。

捕获异常的关键字和格式
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

throw: 当问题出现,程序通过throw抛出一个异常。
catch: 在你想要处理问题的地方,通过异常处理程序捕获异常。
try: try 块中的代码标识将被激活的特定异常。它后面允许跟着一个或多个 catch 块。

1.单类型异常捕获

try { 
    //保护区,就是可能会出错的代码
}

catch( ExceptionName e1 ) {

// 出错后,通过异常处理程序捕获异常
}
2.多类型异常捕获(使用多个catch捕获不同类型的异常)

try { 
    //保护区,就是可能会出错的代码
}

catch( ExceptionName e1 ) {

// 出错后,通过异常处理程序捕获异常
//如果try在不同场景会抛出不同异常,此时可尝试罗列多个 catch 语句,用于捕获不同类型异常
}
catch( ExceptionName e2 ) {
// catch 块
}
catch( ExceptionName eN ) {
// catch 块
}

3.全部类型捕获(缺点是你无法知道错误类型,只知道发送了错误)

try{
    //保护区
}
catch(...)  //这里使用...,表示会捕获所有类型的异常都会
{
    // catch 块
}
————————————————
原文链接:https://blog.csdn.net/m1059247324/article/details/116228823

野指针是什么?如何避免野指针?

野指针概念:指向内存被释放的内存或者没有访问权限的内存的指针。

如何避免野指针:

对指针进行初始化

①将指针初始化为NULL。

char * p = NULL;

②用malloc分配内存

char * p = (char * )malloc(sizeof(char));

③用已有合法的可访问的内存地址对指针初始化

char num[ 30] = {0};

char *p = num;

指针用完后释放内存,将指针赋NULL。

delete(p);

p = NULL;

什么是内存泄漏,如何检测·,如何解决?

内存泄漏定义:

内存泄漏(Memory Leak)是指程序中已动态分配(malloc,new出来的)的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

(1)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过 malloc,realloc new 等从堆中分配的一块内存,再是完成后必须通过调用对应的 free 或者 delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak 。

(2)系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET 等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

检测方法:

⑴ 使用工具软件 BoundsChecker,BoundsChecker 是一个运行时错误检测工具,它主要定位程序运行时期发生的各种错误。

⑵ 调试运行 DEBUG 版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号(集成开发环境 OUTPUT 窗口),综合分析内存泄漏的原因,排除内存泄漏。

如何解决:

解决内存泄漏最有效的办法就是使用智能指针(Smart Pointer)。使用智能指针就不用担心这个问题了,因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放,这样就不用担心内存泄漏的问题了。

虚拟内存?

什么是虚拟内存?

百度百科:虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

《操作系统》:虚拟存储技术的基本思想时利用大容量外存来扩充内存,产生一个比有限的实际内存空间大得多的、逻辑的虚拟空间,简称虚存,以便能够有效地支持多道程序系统的实现和大型程序运行的需要,从而增强系统的处理能力。

一句话概括:虚拟内存是一种内存管理技术,是虚拟的、逻辑上存在的存储空间。

为什么要使用虚拟内存?

我们先回顾一下程序执行的原理,首先程序是运行在内存中的,程序运行时会将保存在硬盘上的程复制到RAM内存(载入内存),然后CPU执行内存中的程序代码。

如果执行的程序占用内存很大或很多,或同时执行多个程序,就会导致内存消耗殆尽。从而导致程序执行异常或崩溃。

虚拟内存工作原理

当进程开始运行时,操作系统为每个进程分配一块虚拟内存空间,这个空间比实际物理内存大得多。同时先将一部分程序装入物理内存,另一部分暂时留在物理外存。

并且,这个虚拟内存被分成许多小块,称为页(通常大小为4KB)。每个页可以映射到实际的物理内存或磁盘上。

当程序需要访问某个虚拟内存页时,操作系统会先查看这个页是否已经加载到了物理内存中。如果没有,就会发生一个叫做“页错误”的情况,系统会把这个页从磁盘读取到内存中,并更新页表。

如果物理内存已经满了,操作系统可能会使用一些算法(比如最近最少使用)来决定将哪些页换出到磁盘,为新的页腾出空间。

总的来说,虚拟内存技术使得程序能够使用比实际物理内存更大的内存空间,同时通过将数据放在磁盘上来扩展内存空间,从而提高了系统的性能和稳定性。

————————————————

原文链接:https://blog.csdn.net/blankmargin/article/details/118534348

Static 关键字的作用

static有两个作用,分别是隐藏和唯一。我们将static修饰的对象分为函数和变量来进行讨论。

1,隐藏性质

  当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用static隐藏这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。

  • 修饰模块内(但在函数体外)的变量,一个被声明为静态的变量可以被模块内所用函数访问,但不能在其他文件中访问,此时static的作用为隐藏。
  • 修饰模块内的函数,一个被声明为静态的函数只可被这一模块内的其它函数调用,在其他文件中不可见,此时static的作用为隐藏
2,唯一性质
  •  修饰类中成员变量

      用static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例。因此,static成员必须在类外进行初始化,而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。

     此时static的作用为唯一,因为所有对象维护同一个变量实例。 

  •   修饰函数中的变量

此时,变量具有唯一性质,所有函数维护同一个变量实例。

  •   修饰成员函数

     用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量。

    与static成员变量一样,此时static修饰成员函数的作用为唯一,因为所有对象使用同一个函数。 

补充:对于类中的静态成员变量和静态成员函数,访问他们可以不需要创建对象,

使用类名+作用域限定符 (::)  即可访问

Const关键字的作用

const有两个功能,分别是常量和只读

1,常量

const int a=10;

在定义普通变量时,const的作用是使变量成为一个常量

2,只读

void function(const int a)
{
cout<<a<<endl;
}

在定义函数参数中的变量时,const的作用是使变量不得被更改

指针

指针常量和常量指针

  1. 指针常量(Pointer to Constant)

    • 指针常量是指指针本身的值(即它指向的内存地址)是不可变的,一旦指针被初始化后,它就不能再指向其他地址。但是,通过这个指针,我们可以修改它指向的内存地址中的内容。
    int* const ptr; // 常量指针

    上述代码表示 ptr 是一个指向常量的指针常量,它指向的地址不能修改,但可以通过 ptr 来修改所指向的内容。

  2. 常量指针(Constant Pointer)

    • 常量指针是指指针所指向的内容是不可变的,不能通过这个指针修改它所指向的内存地址中的内容。但指针本身的值(即它指向的内存地址)是可以改变的,可以指向其他地址。
    const int *ptr = &a;

    上述代码表示 ptr 是一个常量指针,它指向的内容不能修改,但可以通过其他方式修改 ptr 所指向的地址。

总结:const在*的左边,是指针常量,const在*的右边,是常量指针

函数指针和指针函数

函数指针

函数指针是一个指向函数的指针变量,定义的格式如下:

int (*p)(int x, int  y);  //注意:这里的括号不能掉,因为括号()的运算优先级比解引用运算符*高

指针函数

指针函数:指的是函数的返回值是一个指针,比如我的函数返回的是一个指向整数int的指针,定义格式如下:

int *p(int a,int b); //注意这里的*与P之间是没有括号的,所以含义是函数p(int,int)会返回一个(int *)指针

利用函数指针实现函数回调

其实函数指针更重要的意义在于函数回调。

具体使用时如下


BOOL compareGreater(int number, int compareNumber) {
    return number > compareNumber;
}

BOOL compareLess(int number, int compareNumber) {
    return number < compareNumber;
}

void compareNumberFunction(int *numberArray, int count, int compareNumber, BOOL (*p)(int, int)) 
{
    for (int i = 0; i < count; i++) 
    {
        if (p(*(numberArray + i), compareNumber)) //通过函数指针调用比较函数
        {
            printf("%d\n", *(numberArray + i));
        }
    }
}
int main() {
 
    int numberArray[5] = {15, 34, 44, 56, 64};
    int compareNumber = 50;
    // 大于被比较数字情况:
    compareNumberFunction(numberArray, 5, compareNumber, compareGreater);
    // 小于被比较数字情况:
    compareNumberFunction(numberArray, 5, compareNumber, compareLess);
 
    return 0;
}

————————————————                       
原文链接:https://blog.csdn.net/qq_27825451/article/details/103081289

引用与指针区别

两者的定义和性质不同

指针是一个变量,存储的是一个地址,指向内存的一个存储单元;

引用是原变量的一个别名,跟原来的变量实质上是同一个东西,

进而衍生出一系列的不同,如自增运算,sizeof等

引用的本质:引用的本质在c++内部实现是一个指针常量,引用一旦被初始化之后就不能更改。

因此引用必须初始化,且不能更改

new、delete 和 malloc、free 实现原理与区别

new、delete 和 malloc、free 的区别区别:

  • 性质不同,​malloc 与free 是C++/C 语言的标准库函数,new/delete 是C++的运算符
  • 功能有区别,maloc/free 无法满足对象初始化和析构的要求。无法调用对象的构造函数与析构函数。而new/delete可以调用对象的构造函数与析构函数。
  • 使用不同,使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的尺寸;
  • 处理异常的方式不同,new 分配失败的时候会直接抛出异常,malloc 分配失败会返回 NULL; 
  • -返回值不同,new 分配成功后会返回对应类型的指针,而 malloc 分配成功后会返回 void * 类型;

malloc实现原理:

1,如果用户申请的内存小于 128 KB,则通过 brk() 申请内存。而brk()的实现的方式很简单,就是通过 brk() 函数将堆顶指针向高地址移动,获得新的内存空间。如下图:

2,malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

new/delete实现原理:

new(运算符) 会调用operator new()函数,然后operator new()会调用malloc()函数

delete(运算符) 会调用operator delete()函数,然后operator delete()会调用free()函数

operator new()函数用于动态内存分配但不会调用对象的构造函数

operator delete()函数用于内存释放但不会调用对象的析构函数

C和C++中结构体的区别

  • C的结构体内不允许有函数,而C++的结构体内部允许有成员函数(允许有构造函数、析构函数和this指针),且允许这个函数是虚函数。C++中struct增加了访问权限,且可以和类一样有成员函数。
  • c中的struct是没有权限设置的。C的结构体对内部成员变量的访问权限只能是public,而C++允许public,private,protected三种。
  • C的结构体是不可以继承的,C++的结构体是允许从其他结构体或者类继承的。


原文链接:https://blog.csdn.net/qq_26822029/article/details/82902161

C++中类和结构体的区别

 C++中的结构体(struct)和类(class)有一些相似之处,但也存在一些关键的区别。以下是结构体和类之间的主要区别:

1、默认访问权限:

结构体的成员默认访问权限是公共的(public),这意味着结构体的成员在外部可以直接访问。
类的成员默认访问权限是私有的(private),这意味着类的成员在外部不能直接访问,需要通过公共的成员函数来访问。
2、成员函数:

类可以包含成员函数,这些函数可以操作类的私有成员,并且可以实现类的行为和功能。
结构体也可以有成员函数,但是它们的主要目的是为了实现一些操作,而不是定义类似于类的行为。
3、继承:

类可以通过继承实现子类与父类之间的关系,可以使用公共、保护或私有继承来控制成员的访问权限。
结构体也可以继承,但由于其成员默认是公共的,继承可能导致访问权限问题。
4、构造函数和析构函数:

类可以拥有构造函数和析构函数,用于对象的初始化和清理。
结构体也可以有构造函数和析构函数,但是它们的使用场景通常是比较简单的数据封装。
5、默认成员访问标签(Access Labels):

在类中,可以使用访问标签(public、private、protected)来指定成员的访问权限。
在结构体中,无法使用访问标签来指定成员的访问权限,所有成员都默认是公共的。
6、 使用场景:

结构体的使用场景:

用于存储一组相关的数据,但没有复杂的操作和逻辑。
当数据的封装比行为更重要时,例如在处理图形、坐标、日期等数据时。
当你需要将数据序列化/反序列化为二进制或其他格式时。
作为轻量级的数据容器,适用于性能要求较高的情况。
类的使用场景:

当你需要封装数据并附加操作和行为时,类更适合,因为它允许你将数据和操作封装在一起。
在面向对象编程中,用于建模现实世界的对象,例如人、车辆、银行账户等。
当你需要使用继承和多态来实现代码的扩展和重用。
为了实现更复杂的数据结构,如链表、树、图等。
————————————————
原文链接:https://blog.csdn.net/qq_39350172/article/details/132523467

面向对象

面向对象的三个基本特征

面向对象的三个基本特征是:封装、继承、多态。其中,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!

什么是封装?

封装是把客观事物抽象成类,在类中整合该事物体的属性和动作,并对进行权限划分。

封装封装可以隐藏实现细节,使得代码模块化。

什么是继承?

继承是指这样一种能力:派生类可在无需并在无需重新编写代码的情况下,使用基类的所有功能,并且能对这些功能进行扩展。

继承的实现方式?

继承概念的实现方式有三类:实现继承、接口继承和可视继承。

1. 实现继承是指使用基类的属性和方法而无需额外编码的能力;

2. 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

3. 可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。

继承注意事项:构造函数,析构函数,赋值运算符重载函数不能被继承

C++中,并不是所有的成员函数都能被子类继承,有三类成员函数不能被子类继承,分别是:构造函数(包括拷贝构造)、析构函数、赋值运算符重载函数。

一,构造函数   

构造方法用来初始化类的对象,与父类的其它成员不同,它不能被子类继承(子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造方法)。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。   

二,析构函数   

析构函数也不会被子类继承,只是在子类的析构函数中会调用父类的析构函数。

如果没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
三,赋值运算符重载函数   

赋值运算符重载函数也不会被子类继承,只是在子类的赋值运算符重载函数中会调用父类的赋值运算符重载函数。
————————————————
原文链接:https://blog.csdn.net/u010765526/article/details/106751977

什么是多态?

多态是指用相同的方法去操作对象,对象收到不同的消息会产生不同的动作,从而产生不同的结果。

C++支持两种多态性:编译时多态性,运行时多态性。

  • 静态多态: 函数重载,运算符重载和模板
  • 动态多态: 派生类和虚函数实现运行时多态

  c++的动态多态性用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

虚函数,虚函数表

派生类和虚函数实现动态多态的原理:
  1. 虚函数表(vtable)

    • 虚函数表是一个存储了虚函数地址的表格,每个含有虚函数的类都有一个对应的虚函数表。
    • 虚函数表中的每一项都是一个指向虚函数的指针,用于指向对应虚函数的实际地址。
    • 当一个类包含虚函数时,编译器会在该类的内存布局中添加一个指向虚函数表的指针(vptr),该指针指向该类的虚函数表。
    • 派生类会继承基类的虚函数表,并根据需要重写其中的虚函数,从而实现多态性。
  2. 虚函数指针(vptr)

    • 虚函数指针是一个指向虚函数表的指针,通常存在于对象的内存布局中。
    • 虚函数指针指向对象所属类的虚函数表,以便在运行时根据对象的实际类型调用正确的虚函数。

实现虚函数的核心是在对象的内存布局中添加一个虚函数表(vtable),其中存储了指向各个虚函数的指针。当通过基类指针或引用调用虚函数时,实际上是通过虚函数表来确定要调用的函数的地址,这样可以在运行时根据对象的实际类型来调用正确的函数。

当一个类包含虚函数时,编译器会自动为其生成虚函数表,并在对象的内存布局中添加一个指向该虚函数表的指针。这个指针通常被称为虚函数表指针(vptr),它指向对象所属类的虚函数表。因此,当调用一个虚函数时,实际上是通过对象的虚函数指针找到对应的虚函数表,并根据函数的索引在虚函数表中找到相应的函数地址,然后调用该函数。这样可以实现在运行时根据对象的实际类型来调用正确的虚函数,从而实现多态性。

补充:虚函数表的建立发生在编译阶段,而不是运行时。当类被编译时,编译器会在类的内存布局中创建虚函数表,并将虚函数的地址填充到对应的表项中。同时,编译器会根据需要在类的内存布局中添加一个指向虚函数表的虚函数指针。因此,虚函数表和虚指针的建立是在编译时完成的。

虚函数注意事项:
C++中,普通函数,友元函数,构造函数,内联函数,静态成员函数不能声明为虚函数

什么样的函数不能声明为虚函数?1)不能被继承的函数。2)不能被重写的函数。

1)普通函数

普通函数不属于成员函数                    ,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。

而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。

2)友元函数

友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

3)构造函数

首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。

4)内联成员函数

我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。

5)静态成员函数

首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。
————————————————
原文链接:https://blog.csdn.net/nie19940803/article/details/77427219

C++基础:什么是C++的多态性_多态性c++-CSDN博客

虚析构的作用

当用一个基类的指针删除一个派生类的对象时,如果析构函数不是虚函数,则不会调用派生类的析构函数。因此虚析构的作用便是,当用一个基类的指针删除一个派生类的对象时能调用派生类的析构函数(派生类中可能有堆内存需要释放,需要调用析构函数)。

纯虚函数

什么是纯虚函数 纯虚函数的作用 如何定义使用纯虚函数  

一 定义:

纯虚函数是一种特殊的虚函数,它的一般格式如下: 

  class <类名> 

  { 

  virtual <类型><函数名>(<参数表>)=0; 

  … 

  }; 

  在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。

二 引入原因:

1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 

  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 

  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

三 特性:

1、多态性

  指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。

  a.编译时多态性:通过重载函数实现

  b 运行时多态性:通过虚函数实现。

  2、虚函数

  虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载

  3、抽象类

  包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。

多继承、菱形继承、虚继承等

多继承语法

C++允许一个类继承多个类

语法:

class 子类:继承方式 父类1,继承方式 父类2
  • 1

多继承可能会引发父类中有同名成员出现,需要加作用域区分

菱形继承

菱形继承概念

两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承称为菱形继承,或者钻石继承。

典型的菱形继承案例

菱形继承问题

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 草泥马继承动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

虚继承

1、什么是虚继承?

虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

2、虚继承的语法是?

在派生类继承基类时(即在声明派生类的时候),加上一个virtual关键词则为虚拟继承.

class 派生类名:virtual 继承方式  基类名

{

。。。。。。

}

class 派生类: virtual 基类1,virtual 基类2,...,virtual 基类n

{

...//派生类成员声明

};

说明:在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。

4、执行顺序及优先级

首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;

执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;

执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;

执行派生类自己的构造函数;

析构以与构造相反的顺序执行;

从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。

虚继承原理:C++虚继承的实现原理、内存分布、作用_虚继承基类与派生类对象的转换原理及内存分配结构-CSDN博客

----------------泛型-----------------

如何理解泛型?

泛型是指通过编写一些通用的数据结构和算法,也就是说这些代码的实现与数据类型无关,因此我们可以用各类数据类型来使用这些数据结构和算法。在c++中,是利用模板这个概念来实现泛型的,我们用template+typename/class来定义模板,最典型的泛型例子就是c++为我们提供的一系列数据结构模板和算法模板,如vector,map和sort,qsort,foreach等。

---------------c++11----------------

NULL与nullptr的区别

在 C++11 之前,我们通常使用 NULL 来表示空指针。

然而,在 C++ 中,NULL 的定义实际上是一个整数值 0,而不是一个真正的指针类型。

在函数重载和模板编程中这可能会导致一些问题和歧义。

为了解决这个问题,C++11 引入了一个新的关键字 nullptr,用于表示空指针。

nullptr 是一种特殊类型的字面值,类型为 std::nullptr_t,定义为: typedef decltype(nullptr) nullptr_t,可以隐式转换为任何指针类型。

与 NULL 不同,nullptr 是一个真正的指针类型,因此可以避免一些由于 NULL 是整数类型而引起的问题。

原文链接:

 https://csguide.cn/cpp/modern_cpp/nullptr.html#%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD

RAII(资源获取即初始化)思想

C++RAII(Resource Acquisition Is Initialization 资源获取即初始化)是什么?

C++RAII是一种编程技术,旨在通过对象的生命周期来管理资源的获取和释放。RAII的核心思想是:将资源的生命周期与对象的生命周期绑定在一起,在对象的构造函数中获取资源,在对象的析构函数中释放资源。这样可以确保资源在任何情况下都会被正确释放,从而避免资源泄漏。

RAII技术在C++中使用得广泛,其中最常见的应用是使用智能指针,如shared_ptr和unique_ptr,来管理动态内存,以确保内存的自动回收。RAII技术还可以用于管理其它类型的资源,如文件句柄、网络连接、互斥锁等。RAII的优点是使得资源管理变得简单、安全、可靠,并且代码易于维护。

RAII的原理

RAII利用了C++语言的一个特性:对象的构造函数在创建对象时自动调用,而析构函数在对象销毁时自动调用,同时,在对象的构造函数中获取资源,在对象的析构函数中释放资源。通过将资源的生命周期与对象的生命周期绑定在一起,,RAII确保了资源的正确管理。
————————————————
原文链接:https://blog.csdn.net/Dontla/article/details/129069299


智能指针:shared_ptr、weak_ptr、unique_ptr的使用和原理

1.unique_ptr是c++11版本库中提供的智能指针,它直接将拷贝构造函数和赋值重载函数给禁用掉,因此,不让其进行拷贝和赋值。

2. shared_ptr的原理
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:

shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
引用计数是用来记录资源对象中有多少个指针指向该资源对象。

3.weak_ptrd的使用

shared_ptr固然好用,但是它也会有问题存在。假设我们要使用定义一个双向链表,如果我们想要让创建出来的链表的节点都定义成shared_ptr智能指针,那么也需要将节点内的_pre和_next都定义成shared_ptr的智能指针。如果定义成普通指针,那么就不能赋值给shared_ptr的智能指针。

所以在定义双向链表或者在二叉树等有多个指针的时候,如果想要将该类型定义成智能指针,那么结构体内的指针需要定义成weak_ptr类型的指针,防止循环引用的出现。————————————————
原文链接:https://blog.csdn.net/sjp11/article/details/123899141

(1) shared_ptr 

        实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。 

        1) 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针;

         2) 每次创建类的新对象时,初始化指针并将引用计数置为1;

         3) 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;

         4) 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;

         5) 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

(2) unique_ptr 

       unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。

(3) weak_ptr 

      weak_ptr:弱引用。 引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
————————————————
原文链接:https://blog.csdn.net/lizhentao0707/article/details/81156384

值类别

右值引用,移动构造,完美转发

int a=10;
int& b=a;          //左值引用
int&& c=10;        //右值引用
const int & d=a;   //常量左值引用
const int & d=10;  //可行的,加了const的左值引用可以接收右值
const int&& e =10; //常量右值引用

右值引用 int &&

右值引用可以使将要被释放的临时数据多存活一段时间,

同时观察代码,执行这段语句,则在make_x函数中,会执行一次拷贝构造,将创建的x拷贝给返回值x,然后回到,又会将返回值拷贝给x3,那么就会执行两次拷贝构造,而我们会发现,make_x函数中的x和返回值x都是右值,是临时的数据,用完即释放,为这种临时数据而进行拷贝构造,很不划算。

于是我们可以使用右值引用,这样就能减少一些拷贝构造

移动构造

移动构造中,记得将交出所有权的指针赋值为NULL,因为在在析构函数中,还会delete,如果不这样做,会导致两次delete同一个地址。

被delete后的指针p的值(地址值)并非就是NULL,而是随机值。 也就是被delete后,如果不再加上一句p=NULL,p就成了“野指针”,在内存里乱指一通。而再次delete便会出现访问权限的问题。

观察此构造函数,假如想利用原pool,构造一个新的和原pool一模一样的newpool,深拷贝构造的话是开辟一个新空间,并将内容复制一份放进来。而移动构造则是直接将原pool的内容搬到newpool的空间里去,这也就是转移所有权,原pool内容的所有权交给newpool了。

lambda和inline

一、inline内联函数

修饰前的声明定义:在头文件声明,在源文件定义
修饰后的声明定义:声明与定义不分开,在头文件声明定义
修饰前的调用方法:通过函数地址调用函数体
修饰后的调用方法:在函数调用处插入函数体,类似与C语言的宏展开
首先需要明确的是,函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。因此,如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视。内联函数的调用方式是在函数调用处插入函数体,那么也就没有了函数地址,也就易得内联函数的声明定义是不能分开的。
综上:内联函数适用于函数体较短,且无递归/循环的函数。

二、lambda 匿名函数

Lambda 表达式是 C++11 引入的一种匿名函数的形式,允许在需要函数对象的地方使用内联的匿名函数。Lambda 表达式的语法如下:

[capture](parameters) -> return_type { body }

各部分说明如下:

  • capture(捕获列表):用于捕获局部变量以供 Lambda 表达式使用。捕获列表可以为空,也可以包含一个或多个捕获变量,以逗号分隔。捕获列表使用方括号([])括起来,具体的形式有:

    • []:不捕获任何变量。
    • [var]:按值捕获变量 var。
    • [&var]:按引用捕获变量 var。
    • [=]:按值捕获所有局部变量。
    • [&]:按引用捕获所有局部变量。
    • [=, &var]:按值捕获所有局部变量,但按引用捕获变量 var。
    • [var1, var2]:按值捕获变量 var1 和 var2。
    • 等等...
  • parameters(参数列表):Lambda 表达式的参数列表,类似于函数的参数列表,可以为空或包含一个或多个参数。

  • return_type(返回类型):Lambda 表达式的返回类型,可以省略,如果省略则根据表达式的内容进行推断。

  • body(函数体):Lambda 表达式的函数体,类似于普通函数的函数体,包含了要执行的代码逻辑。

下面是一个简单的示例,演示了 Lambda 表达式的基本用法:

#include <iostream>

int main() {
    // Lambda 表达式示例:捕获局部变量并输出
    int x = 10;
    int y = 20;
    auto lambda = [x, &y]() {
        std::cout << "x = " << x << ", y = " << y << std::endl;
    };

    lambda(); // 调用 Lambda 表达式

    return 0;
}

在这个示例中,Lambda 表达式 [x, &y]() 捕获了变量 x 按值捕获,变量 y 按引用捕获。Lambda 表达式中的函数体输出了捕获的变量 x 和 y 的值。

lambda表达式就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象。以更直接的方式撰写程序代码,具有较高的可读性和可维护性。lambda的捕获列表只能捕获局部非static变量,因此lambda的作用范围是有限的。
由以上两个例子可知:lambda表达式适用于只在某处临时调用的函数,一般配合泛型算法使用。
————————————————                     
原文链接:https://blog.csdn.net/wangjie112358/article/details/131155222

仿函数

什么是仿函数?

顾名思义,仿函数并非真正的函数,而是在类中重载()运算符,使其具有类似函数的功能

class my_add
{
public:
int operator()(int a,int b)
{
return a+b;
}
};

仿函数(Functor)是 C++ 中的一个概念,指的是能够像函数一样被调用的对象。它通常是一个类,重载了函数调用运算符 operator()。仿函数的作用包括以下几个方面:

  1. 灵活性:仿函数可以像函数一样被调用,但相比普通函数更加灵活,可以包含状态和行为,并且可以被设计成可定制的行为。这使得仿函数在某些情况下比普通函数更加方便和实用。

  2. 封装性:仿函数可以封装一些具体的行为逻辑,包括数据和方法,将这些行为封装在一个对象中,提高了代码的可维护性和可读性。

  3. 适应性:仿函数可以适应各种不同的调用场景,包括作为参数传递给算法函数、作为函数对象存储在容器中等。这使得仿函数在泛型编程和 STL 中的使用非常普遍。

  4. 状态保存:由于仿函数是一个对象,因此它可以保存状态。这意味着可以通过仿函数来实现某些需要状态的操作,比如在算法中需要记住某些中间结果的情况。

  5. 可定制性:仿函数可以被设计成可定制的,允许用户自定义其行为和逻辑。这使得仿函数可以灵活地应对不同的需求和场景。

总的来说,仿函数作为 C++ 中的一个重要概念,具有灵活性、封装性、适应性、状态保存和可定制性等特点,经常在泛型编程和 STL 中的使用。

例如,for_each是一种模板函数,使用时需要使用仿函数

标准库中提供了一个名为 std::for_each 的函数模板,可以用于遍历容器中的元素,并对每个元素执行指定的操作。

std::for_each 的基本用法如下:

#include <algorithm>
#include <iostream>
#include <vector>

// 定义一个函数对象,用于打印元素
struct Print {
    void operator()(int num) const {
        std::cout << num << " ";
    }
};

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

    // 使用 for_each 遍历 vector 中的元素,并调用 Print 的 operator() 函数打印元素
    std::for_each(vec.begin(), vec.end(), Print());

    return 0;
}

这段代码会输出 vector 中的所有元素:1 2 3 4 5。

std::for_each 接受三个参数:

  • 要遍历的容器的起始迭代器、
  • 要遍历的容器的结束迭代器,
  • 要执行的操作(函数对象)。

在遍历过程中,对容器中的每个元素依次调用操作(函数对象)。

一些关键字,override(覆盖), overload(重载)藏), virtual,extern,volatile、decltype

override(覆盖), overload(重载), overwrite

1. Overload(重载)

  重载的概念最好理解,在同一个类声明范围中,定义了多个名称完全相同、参数(类型或者个数)不相同的函数,就称之为Overload(重载)。重载的特征如下:

(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

2. Override(覆盖)

  覆盖的概念其实是用来实现C++多态性的,即子类重新改写父类声明为virtual的函数。Override(覆盖)的特征如下:

(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数列表完全相同;
(4)基类函数必须有virtual 关键字。

volatile

使用 volatile 声明的变量的值的时候,volatile 指出变量是随时可能发生变化的,系统必须从变量的内存地址中读取,即使它前面的指令刚刚从该处读取过数据(会存放在寄存器中,因此volatile可以避免系统直接读取寄存器中的值,这在并发情况下会引起问题)。

C++中volatile关键字的使用详解_volatile cpp-CSDN博客

extern

使得在其他文件中可以使用全局变量和全局函数,也使得能在c++文件中调用c方式编译的函数

一、定义和声明的区别

声明:用来告诉编译器变量的名称和类型,而不分配内存,不赋初值。

定义:为了给变量分配内存,可以为变量赋初值。

注:定义要为变量分配内存空间;而声明不需要为变量分配内存空间。

二、extern用法

extern是一种“外部声明”的关键字,字面意思就是在此处声明某种变量或函数,在外部定义。

2.1 extern 函数
为什么要用extern 函数呢?直接#include相应的头文件不可以嘛?

例子,如b.c 想调用a.c 中的fun函数,有两种方法:

方法1:include 头文件,即直接 #include "a.h" 

方法2:  extern 方法 ,extern void fun(...)这句在调用文件中使用,表示引用全局函数fun(),当然,函数默认是全局的。

优点:不inlcude delayms.h就不会引入大量头文件,进而不会引入大量的无关函数。这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。 在makefile中需要led.o和delay.o写在一起,否则link的时候找不到delayms而报错。

2.2 extern 变量


如果文件b.c需要引用a.c中变量int v,就可以在b.c中声明extern int v,然后就可以引用变量v。能够被其他模块以extern修饰符引用到的变量通常是全局变量。注意,extern int v可以放在b.c中的任何地方,具体作用范围和局部变量相同。

extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

2.3

2.3 在C++文件中调用C方式编译的函数

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。 

// c_functions.h
#ifdef __cplusplus
extern "C" {
#endif

void c_function();

#ifdef __cplusplus
}
#endif
2.4 注意:声明可以多次,定义只能一次。
extern int i; //声明,不是定义
int i; //声明,也是定义

三、通俗讲解

🍍 在函数内部:

int a;// 定义,作为局部变量分配了空间

extern int a;// 声明

🍍 在函数外部与在头文件中:

int a;// 全局变量,声明兼未初始化定义,详见参考文献

extern int a;// 声明
————————————————
原文链接:https://blog.csdn.net/qq_41709234/article/details/122984203

【014 关键字】一文彻底搞懂extern用法-CSDN博客

auto 与 decltype()

两者都是类型推导

auto: 主要在定义变量时使用,使用auto声明的变量必须要进行初始化,atuo a=10;

decltype:可以实现对复杂表达式的类型推导

auto 在声明时,根据数据推导出类型

C++11中auto并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。使用语法如下:

auto 变量名 = 变量值;
auto x = 3.14;      // x 是浮点型 double
auto y = 520;       // y 是整形 int
auto z = 'a';       // z 是字符型 char
auto nb;            // error,变量必须要初始化
auto double nbl;    // 语法错误, 不能修改数据类型   
decltype(表达式) 获取表达式的类型

在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的decltype关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:
decltype (表达式)
decltype 是“declare type”的缩写,意思是“声明类型”。decltype的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:

int a = 10;
decltype(a) b = 99;                 // b -> int
decltype(a+3.14) c = 52.13;         // c -> double
decltype(a+b*c) d = 520.1314;       // d -> double

可以看到decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。

auto+decltype实现返回值后置

语法:

auto func(参数1, 参数2, ...) -> decltype(参数表达式)

auto 会追踪 decltype() 推导出的类型例:

#include <iostream>
using namespace std;

template <typename T, typename U>
// 返回类型后置语法
//在模板函数中,返回值与模板参数有关时,我们需要通过推导数据类型,来定义返回值
//若使用auto行不通,因为auto是在定义变量时使用的,使用decltype也行不通,
//因为定义返回值时,函数参数还未声明
//因此我们采用返回值后置的方法,用decltype在函数参数声明之后去推导类型,并用auto去接收
auto add(T t, U u) -> decltype(t+u) 
{
    return t + u;
}

int main()
{
    int x = 520;
    double y = 13.14;
    // auto z = add<int, double>(x, y);
    auto z = add(x, y);		// 简化之后的写法
    cout << "z: " << z << endl;
    return 0;
}

-------------------------------------------------------------------------------
作者: 苏丙榅
链接: https://subingwen.cn/cpp/autotype/?highlight=dec

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值