Bootstrap

C++基础部分

C++基础

2.命名空间

  1. 命名空间里面可以定义变量、函数、类型
  2. 命名空间可以嵌套
  3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

2.1命名空间的概念和意义

命令冲突问题:

  1. 我们自己定义的变量,函数可能与库里面重名冲突
  2. 多人协作代码的时候两个人之间的代码命名会冲突
#include<stdio.h>
#include<stdlib.h>
//命名冲突问题
int rand=0;
int main(){
    printf("%d\n",rand);
	return 0;
}

CPP如何解决?CPP提出了命名空间

#include<stdio.h>
#include<stdlib.h>

namespace YCB{
    int rand=0;
}
int main(){
    printf("%d\n",rand);
    printf("%d\n",YCB::rand);
    return 0;
}
  • 编译器优先在局部找,找不到再去找全局的
#include<stdio.h>
#include<stdlib.h>

namespace YCB{
    int rand=0;
}
int a=0;
int main(){
	int a=1;
    printf("%d\n",::a);///在全局域里面找,C语言就有
    printf("%d\n",a);//编译器优先在局部找,找不到再去全局的
    return 0;
}
  • 命令空间里面的一样还是全局变量,放在静态区
#include<stdio.h>
#include<stdlib.h>
//定义了一个命名空间,他们就是全局变量,性质和全局变量一模一样。看成全局变量即可。不过套了一下壳子.全局变量当然是直接初始化了。
namespace YCB{
    int rand=0;
    int a=1;
}
void f(){
    //局部域
}
int a=0;
int main(){
	int a=1;
    printf("%d\n",::a);///在全局域里面找,C语言就有
    printf("%d\n",a);//编译器优先在局部找,找不到再去全局的
    printf("%d\n",YCB::a);
    return 0;
}
  • 命名空间里面可以定义变量、函数、类型

每个命名空间之间可以定义同名函数,不会冲突

namespace YCB{
    int a=10;
    int b=20;
    int Add(int a,int b)
    {
        return a+b;
    }
	int Sub(int a,int b)
    {
		return a-b;
    }
    struct Node{
        struct Node *next;
        int val;
    }
}///不需要;
int main(){
    struct YCB::Node node;
}
  • 命名空间之间可以嵌套
namespace YCB
{
    int a,b;
    int add(int a,int b) return a+b;
    namespace S{
        int c,d;
        itn sub(int c,int d) return c-d;
    }
}
  • 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
//List.h
#pragma once

namespace YCB{
    Struct ListNode{
        
    }
    void ListInit();
    void ListPushBack();
}
//List.cpp
#include"List.h"
namespace YCB{
    void ListInit(){
        ...
    }
    void ListPushBack(){
        ...
    }
}
//两者会合到一起

2.2三种使用方式

  • 加命名空间名称及作用域限定符

    • 指定作用域能做到最好的命名隔离,但是使用不方便

    • int main()
      {
      	printf("%d\n",N::a);
          return 0;
      }
      
  • 使用using将命名空间中成员引入

    • 用于展开命名空间中常用的。

    • using N::b;
      int main()
      {
      	printf("%d\n",N::a);
          printf("%d\n",b);
      }
      
  • 使用using namespace 命名空间名称引入

    • 全部展开到全局了,隔离失效

    • 正规做项目慎用

    • using namespace N;
      int main()
      {
      	printf("%d\n",N::a);
          printf("%d\n",b);
          Add(10,20);
      }
      
#include<iostream>
using namespace std; //c++库中所有东西都是放到std命名空间的
///全开展,好处方便,但是如果定义了自己的函数和std同名了就不行
int main()
{
    
    
}

3.c++输入&输出

  • > > >> >>是流提取
  • < < << <<是流插入
#include<iostream>
//在日常练习中,不在乎跟库命名冲突
using namespace std; //c++库中所有东西都是放到std命名空间中

int cout=10;//跟库里面冲突
//但是非要定义,一种解决是std::cout,std::endl;
//另一种只展开部分 using std::cout ,using std::endl;

//常用的库里面一些对象或者类型展出来
//项目当中比较常见
using std::cout;
using std::endl;
int main()
{
    std::cout<<"hello world\n";
    std::cout<<"hello world"<<std::endl;
    //自动识别类型:通过函数重载实现
    int i=1;double d=1.11;
    std::cout<<i<<" "<<d<<std::endl;
}

4.缺省参数

缺省参数是声明或定义函数时为参数提供的一个默认值。

void Func(int a=0)//形参的缺省参数-->备胎
{
    cout<<a<<endl;
}
int main()
{
    Func(10);
    Func();
    return 0;
}
4.1全缺省
///全缺省
void Func1(int a=10,int b=20,int c=30)
{
	cout<<"a="<<a<<endl;
	cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
}
int main()
{
	Func1();
    Func1(1);
    Func1(1,2);
    Func1(1,2,3);
}
4.2半缺省
//半缺省(缺省部分参数),必须从右往左连续缺省
void Func2(int a,int b=10,int c=20)
{
	Func2();//error
    //调用时如果要传参必须从左往右依次传参。
    Func2(1);
    Func2(1,2);
    Func2(1,2,3);
}
void StackInit(struct Stack* ps,int capacity=4){
    ps->a=(int*)malloc(sizeof(int)*capacity);
    //
    ps->top=0;
    ps->capacity=capacity;
}
4.3缺省的注意事项
  1. 半缺省参数必须从右往左依次来给出,不能间隔着给

  2. 缺省参数不能在函数声明和定义中同时出现

    1. //a.h
      void TestFunc(int a = 10);
      // a.c
      void TestFunc(int a = 20)
      {}
      // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
      // 实际上在VS编译器中直接报错了。
      
  3. 缺省值必须是常量或者全局变量

5.函数重载

5.1函数重载概念

5.1.1重载的判定

一个函数有多种意义或者多个调用方式

  • 函数名相同
  • 参数不同(满足一个)
    • 类型
    • 个数
    • 类型顺序
      • void func(int i,int j) -->_Z4funcii
      • void func(int j,int i) -->_Z4funcii
      • 两者不是重载
  • 对返回值没有要求,返回值和重载没有关系
int Add(int l,int r)
{
    return l+r;
}
double Add(double l,double r)
{
    return l+r;
}
long Add(long l,long r)
{
    return l+r;
}
void func1(int i,char ch)
{
    
}
void func1(char ch,int i)
{
    
}
void func1()
{
    
}
int func1()//和上一个不构成重载
{

}
int main()
{
    Add(10,20);
    Add(10.0,20.0);
    Add(10L,20L);
    return 0;
}

5.2重载的原理(名字修饰)

  1. 什么是函数重载
  2. c++是如何支持函数重载的?为什么C语言不支持

得从编译链接说起

list.h list.c test.c

  1. 预处理(预编译):头文件的展开/宏替换/条件编译/去掉注释
    1. list.i test.i
  2. 编译 :检查语法,生成汇编代码
    1. list.s test.s
  3. 汇编 :汇编代码转换成二进制的机器码
    1. list.o test.o
  4. 链接 :将两个目标文件链接到一起生成可执行程序(如果当前文件中有函数的定义,那么编译时就填上地址了;如果在在当前文件中只有函数的声明,只能链接的时候去其他的xxx.o符号表中根据函数修饰名字去找,这就是链接的重要工作
test.c

void list_push_back(int x);
void add(int i,int j);
int main()
{
    list_push_back(1);
    return 0;
}
list.c -->list.o
void list_push_back(int x);
void add(int i,int j);
void list_push_back(int x)
{
    printf("%d\n",x);
}
void add(int i,int j)
{
    
}
test.o
    ...
    call list_push_back(?)
    //链接时,这里的问号表示编译时,这个函数我们只有声明,没有定义所以无法找到他的地址
    //表示链接的时候,到其他的目标文件的符号表中去找这个函数的地址
    ...
符号表:
    main:0x31141321
list.o
    list_push_back()
{
    ...
}

符号表:
list_push_back:0x31144332
add:0x3114432

  • C语言是直接用函数名称。两个都是一样的函数名字,无法区分。

因为C++是名字修饰。所以可以。

0000000000000400623<_Z3addii>
   
000000000000040062f<_Z3adddd>
    
   test.o
//   add(1,2);
   call _Z3addii(?)
//   add(1.1,2.2);
   call _Z3adddd(?)
  • C++的函数修饰规则(不同编译器不同规则),但都把参数类型加进去了

_Z+函数长度+函数名+类型首字母

image-20211125210846252

image-20211125211121715

因此函数命名不同了之后链接的时候去其他目标文件中找符号表就能找到对应的函数。

//add(1,2)
call _Z3addii(?)  //link的时候去找?的地址
    //其他目标文件中_Z3addii符号的地址是000000004007dd
//add(1.1,2.2)
call _Z3adddd(?)  //link的时候去找?的地址
    //其他目标文件中_Z3adddd符号的地址是000000004007ff

image-20211125212731369

image-20211125215458388

C语言不支持函数重载,因为编译的时候,两个重载函数,函数名相同,在同一个目标文件.o中,符号表中存在歧义和冲突。同样的函数名两个函数地址。其次链接的时候也存在歧义和冲突,因为他们都是直接使用函数名去标识和查找。而重载函数,函数名相同。

而C++的目标文件符号表中不是直接用函数名来标识和查找函数

  1. 函数名修饰规则(不同编译器下函数修饰名不同)
    1. _Z+函数长度+函数名+类型首字母
      1. image-20211125220352038
  2. 有了函数名修饰规则,只要参数不同,目标文件的符号表里面就不存在二义性和冲突了
  3. 链接的时候,test.o的main函数里面去调用两个重载的函数也是明确的。

5.3extern “C”

vs下实现静态库:

  1. 包含对应目录下的头文件
  2. 在工程属性中配置静态库目录(链接器的常规中),添加静态库(链接器的输入中)

Cpp调C:在Cpp的#include"…/xxx/xx.h"上下加上extern “C”{}

extern "C"
{
    #include"../xx/Stack.h"
}

C调Cpp:在Cpp的.h文件中加extern “C”{ 函数},Cpp静态库就会按照C的规则去处理以下函数。当然重载就要写两个函数了。

特别注意,因为.c包含了.h的头文件,所以头文件包含会将.h部分在c展开,而cpp才认识extern “C”,C部分不认识extern “C”;

  • 第一种做法
#ifdef __cplusplus
extern "C" {
#endif
    cpp函数声明;
#ifdef __cplusplus
}
#endif

引入条件编译,在cpp库中,是识别extern "C"并且按照C的方式进行函数推导。当C对其头文件展开的时候,由于不是cpp,条件编译直接声明成C函数。

  • 第二种做法
#ifdef __cplusplus
	#define EXTERN_C extern "C" 
#else
	#define EXTERN_C
#endif
    EXTERN_C cpp函数声明;
	EXTERN_C cpp函数声明;
	EXTERN_C cpp函数声明;

**也就是说extern “C” 总是在cpp中的,因为只有cpp 认识 extern " C" **

C++程序调用C的库,在C++程序中加extern “C”

C程序调用C++的库,在C++库中加extern “C”

参考文章:https://zhuanlan.zhihu.com/p/361485807

5.3.1C++程序中调用C库

首先为什么C++程序中不能调用C库,会产生链接错误。因为两者对函数名字的命名规则不同,因此C++的链接器会去C模块中查找对应函数,但是找不到。

那我们怎么在C++项目中使用C库模块的?

//util.h
extern "C"
{
    int add(int ,int );
}

通过extern “C”,告诉g++编译器,不要对这些函数进行Name mangling,按照C编译器的方式去生成符号表符号。这样在main.c的目标文件(.o)中,参数列表为两个int类型的add函数名称为_add。链接器可以正确找到util.o中的add函数(他们都是_add)。

不过注意参数列表为两个double类型的add函数名称还是__Z3adddd。

使用 extern ”C“ 的常见情况是使用第三方提供的编译好的静态链接库(.a/.lib),动态链接库(.so/.dll)。通常我们会拿到一个头文件和对应的编译好的库文件。

在头文件中通过条件编译引入 extern “C”。

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

int add(int, int);

#ifdef __cplusplus
}
#endif
gcc -c xxx.c
ar -rc libxxxx.a xxx.o xxx.o
test-static:test.cc 
		g++ -o $@ $^ -I ./mylib/include -L ./mylib/lib -l util -static
.PHONY:clean
clean:
	rm -rf *.o  test-static 

image-20220114183816152

image-20220114193347352

5.3.2C程序中调用C++函数

假设我们有一个C++类 Robot,在文件 robot.hrobot.cpp 中定义。Robot 类中有个成员函数 sayHi() 我们想在C程序中调用这个函数。

robot.h

#pragma once

#include <string>

class Robot
{
public:
    Robot(std::string name) : name_(name) {}

    void sayHi();

private:
    std::string name_;
};

robot.cpp

#include <iostream>

#include "robot.h"

void Robot::sayHi()
{
    std::cout << "Hi, I am " << name_ << "!\n";
}

我们用编译C++代码的方式,使用 g++ 编译器对这个类进行编译,此时类 Robot 并不知道自己会被C程序调用。

g++ -fpic -shared robot.cpp -o librobot.so

接下来用C++创建一个C的接口,定义在 robot_c_api.hrobot_c_api.cpp 中,这个接口会定义一个C函数 Robot_sayHi(const char *name), 这个函数会创建一个类 Robot 的实例,并调用 Robot 的成员函数 sayHi()。

robot_c_api.h

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

void Robot_sayHi(const char *name);

#ifdef __cplusplus
}
#endif

robot_c_api.cpp

#include "robot_c_api.h"
#include "robot.h"

#ifdef __cplusplus
extern "C" {
#endif

// 因为我们将使用C++的编译方式,用g++编译器来编译 robot_c_api.cpp 这个文件,
// 所以在这个文件中我们可以用C++代码去定义函数 void Robot_sayHi(const char *name)(在函数中使用C++的类 Robot),
// 最后我们用 extern "C" 来告诉g++编译器,不要对 Robot_sayHi(const char *name) 函数进行name mangling
// 这样最终生成的动态链接库中,函数 Robot_sayHi(const char *name) 将生成 C 编译器的符号表示。

void Robot_sayHi(const char *name)
{
    Robot robot(name);
    robot.sayHi();
}

#ifdef __cplusplus
}
#endif

同样用编译C++代码的方式进行编译

g++ -fpic -shared robot_c_api.cpp -L. -lrobot -o librobot_c_api.so

img

现在我们有了一个动态链接库 librobot_c_api.so, 这个动态链接库提供了一个C函数 Robot_sayHi(const char *name),我们可以在C程序中调用它了。

main.c

#include "robot_c_api.h"

int main()
{
    Robot_sayHi("Alice");
    Robot_sayHi("Bob");

    return 0;
}

使用C程序的编译方式,用 gccmain.c 进行编译

gcc main.c -L. -lrobot_c_api

img

可以看到 gcc 编译出的函数符号和 librobot_capi.sog++ 编译器编译出的函数符号一致。这样最终在我们的C程序中可以正确的链接到动态库中的Robot_sayHi(const char *name) 函数。

img


image-20220115133443894

注意这个.c文件一定要用g++编译,不然识别不了c++的头文件,导致折腾了很久。

test:test.c
	g++ -o $@ $^ -I ./mylib  -L ./mylib -l robot_c_api 

.PHONY:clean
clean:
	rm -rf test
librobot_c_api.so:robot.o robot_c_api.o
	g++ -shared -o $@ $^    
robot_c_api.o:robot_c_api.cc
	g++ -fPIC -c $<
robot.o:robot.cc
	g++ -fPIC -c $<

.PHONY:clean
clean:
	rm -rf *.o mylibrobot librobot_c_api.so librobot.so output
	 

.PHONY:output
output:
	mkdir -p ./output/include 
	mkdir -p ./output/lib
	cp ./*.so ./output/lib
	cp ./*.h ./output/include
export LD_LIBRARY_PATH=/home/ycb/demo1/back-Cpp-C-extern/Cpp-C-extern/Cpp-C-extern/mylib

image-20220115133129475

5.4相关问题

  • 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
void f(){
     cout<<"f()"<<endl;
}
void f(int a=0){
     cout<<"f(int a)"<<endl;
}
int main(){
    f();//error:调用存在歧义
}
  • C语言中为什么不能支持函数重载?
  • C++中函数重载底层是怎么处理的?
  • C++中能否将一个函数按照C的风格来编译?

6.引用

6.1引用概念

#include<iostream>
using namespace std;
int main()
{
	int a=1;
    int& ra=a; //ra是a的引用,引用也就是别名。a再取了一个名称ra
	int& raa=ra;
}

引用在物理空间上的意义:

image-20210828154708086

引用就是给一个变量再取新的名字。编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间

类型名& 引用变量名(对象名)=引用实体

6.2引用特性

  1. 引用必须在定义时初始化

    1. int a=1;
      int& b;///error
      
  2. 一个变量可以有多个引用

  3. 引用一旦引用一个实体,不能发生变化

    //1.引用必须在定义时必须初始化
    int main(){
        int a=10;
        int &b;
    }
    
    //2.一个变量可以有多个引用
    int main(){
    	int a =10;
    	int&b =a;
    	int&c =a;
    	int&d =b;
    }
    
    //3.引用一旦引用一个实体,再不能引用其他实体
    int main(){
        
    	int a=10;
    	int &b=a;
    	int c=20;
    
    	b=c;///分析:这里是c变成了d的引用?还是d赋值给c(yes)
        
     	   
    }
    

6.3常引用

总结:

  • 引用取别名时,变量访问权限可以缩小,不能放大。
  • 权限的放大和缩小规则:适用于引用和指针。不适用变量之间的赋值。
6.3.1权限的缩小与放大
int main()
{
 int a=0;
 int& b=a; //b的类型是int
 
 const int a=0;
 int&b = a;//error:编译不通过。原因:a是const,但是不能修改,b的类型是int,也就是可读可写,那么逻辑上就会产生矛盾。
 const int& b=a;//right
 
 int c=1;
 int& d=c;
 const int& e =c;行不行?可以->c是可读可写的,e变成别名是只读,逻辑上是可以的。
   
 //变量之间赋值没有权限缩小和放大的关系,引用才有
 const int ci =i ;
 int x=ci;
 
 return 0;
}
int main()
{
	const int* cp1=&a;
    int* p1=cp1;//error: const int* cp1表示cp1指向的内容不能更改。而int* p1=cp1如此赋值表明p1可以修改该块内存。逻辑错误。权限的放大。
    
    int* p2=&c;
    const int* cp2=p2; //权限缩小,ok
}
void f(int &x){
    cout<<x<<endl;
}
int main(){
    const int a=10;
    const int &b=a;
    f(a);//error:权限的放大。
    f(b);
}
void f(const int& x){
    cout<<x<<endl;
}
int main(){
    const int a=10;
    const int& b=a;
    f(a);
    f(b);
}

特别地,当涉及到类与对象的时候,对于this指针的问题。

类中的函数的隐藏this指针修饰对象和普通函数const参数对象直接调用的权限不对等

class A{
  public:  
    double get_avg_score(){};
}
void fun(const A& a){
    a.get_avg_score();/*会报错*/
}
void fun(const A& a) const{
    a.get_avg_score();
}

在类中的函数后面加constthis指针进行修饰即可。


权限的放大和缩小规则:适用于引用和指针。不适用变量之间的赋值。

不用引用的话传参无所谓,只是对象之间的赋值。const对象拷贝给x。

void f(int x){
    cout<<x<<endl;
}
int main(){
	const int a=10;
    const int &b =a;
    f(a);
    f(b);
}
6.3.2产生右值的场景

类型转化,类型截断,类型提升函数返回值,函数传参,都会产生临时变量。

int main()
{
	int i=0;
    double db=i; //隐式类型转换
    double& rdb=i;//error
    float& rf=i;//error:和字节大小无关
    //但是+const就可以
    const double& rd=i;
    const float& rf=i;
}   

隐式类型转换的赋值是怎么产生的?

image-20210828160751889

6.4引用场景

1、引用做参数
  • 输出型参数
  • 提高效率

回顾之前单链表的PushBack部分,我们要注意传递二级指针来处理原来指针变量的值。

void SLiPushBack(STLNode** pphead,SLTDataType x){
 	assert(pphead);
    
    STLNode* newnode=CreateSListNoded(x);
    if(*pphead==NULL){
        *pphead=newnode;
        return;
    }
    else{
        STLNode* tail=*pphead;
        while(tail->next!=NULL) tail=tail->next;
        tail->next=newnode;
    }
}
int main(){
    SLTNode* plist=NULL;
    SListPushBacn(&plist,1);
    SListPushBacn(&plist,2);
    SListPushBacn(&plist,3);
    SListPushBacn(&plist,4);
    
    SListPushBack(plist);
    return 0;
}

有了引用之后,就可以省去一层二级指针。

int main(){
    
	int a=10;
	int& b=a;

	int *p1=&a;
	int *&p2=p1;
}
void SLiPushBack(STLNode*& pphead,SLTDataType x){
 	assert(pphead);
    
    STLNode* newnode=CreateSListNoded(x);
    if(pphead==NULL){
        pphead=newnode;
        return;
    }
    else{
        STLNode* tail=pphead;
        while(tail->next!=NULL) tail=tail->next;
        tail->next=newnode;
    }
}
int main(){
    SLTNode* plist=NULL;
    SListPushBacn(&plist,1);
    SListPushBacn(&plist,2);
    SListPushBacn(&plist,3);
    SListPushBacn(&plist,4);
    
    SListPushBack(plist);
    return 0;
}

再比如做C语言的题的时候给定接口的int* returnSize就是一个输出型参数。

void swap_c(int* p1,int *p2)
{
    int tmp=*p1;
    *p1=*p2;
    *p2=tmp;
}
void swap_cpp(int &r1,int &r2)
{
	int tmp=r1;
    r1=r2;
    r2=tmp;
}
int main()
{
    int a=0;int b=1;
    swap(&a,&b);
    swap_cpp(a,b);
    return 0;
}

前面说到引用定义的时候要初始化,这里引用定义的地方在传参。

2、引用做返回值

总结:

  1. 凡是传值,不管是参数还是址,都会产生拷贝变量。传引用不会。
  2. 一个函数要使用引用返回,返回变量出了这个函数的作用域还存在,就可以使用引用返回,否则不安全。
    1. 全局变量、静态变量等
  3. 函数使用引用返回的好处是什么
    1. 少创建拷贝一个临时对象,提高效率。
    2. 其实还有一个作用,以后再补充。
    3. 修改返回对象如operator[] (已补充–模板初阶模板类)

先来回顾一下传值返回。

所有的传值都会生成一个拷贝

int Add(int a,int b)
{
	int c=a+b;
    return c;
}
int main(){
    int ret=Add(1,2);
    cout<<ret<<endl;
    return 0;
}

image-20211128151853981

我们可以看到调用Add(int,int)函数的过程return c的过程中,将计算出来的c变量的值存到了临时变量%eax寄存器中,然后再传给main函数中的ret变量。

临时变量存在哪里呢?

  1. 如果c如果比较小(4 or 8),一般是寄存器充当临时变量。
  2. 如果c比较大,临时变量放在调用Add函数的栈帧中。

而传引用返回就是不会生成c的拷贝返回,直接返回c的引用

int Add1(int a,int b)
{
    int c=a+b;
    return c;
}
int main()
{
    const int& ret=Add1(1,2);//临时变量具有常性
    Add1(3,4);
    cout<<"Add1(1,2) is:"<<ret<<endl;
}
int& Add2(int a,int b)
{
    int c=a+b;
    return c;
}
int& Add2(int a,int b)
{
    static int c=a+b;
    return c;
}
int main()
{
    int& ret=Add2(1,2);//ret就是c的别名。(实际上是c这块空间的别名).
    //销毁不意味着清除,是没有使用权。
    Add2(3,4);
    cout<<"Add2(1,2) is:"<<ret<<endl;///ret输出为7了。引用返回是不安全的。
    //说明如果返回变量c是一个局部变量时,引用返回是不安全的。
    return 0;
}
img

出了作用域还是返回已经销毁的栈帧(未有使用权)。不能保证原来的结果,其他函数能改这一块的。就会产生问题。

如何解决这个问题?

加static。

void test()
{
    static int a=1;///第二次不执行
    a++;
    printf("%d",a);
}

此时c不在Add2的栈帧。

所以第二次Add(3,4)的时候static int c=a+b是不执行的。这份代码是只能是3。不过只有本函数才能改自己的c。

int Count1()//传值返回
{
    static int n=0;
    n++;
    return n;//返回临时变量
}
int& Count2()//传引用返回
{
	static int n=0;
    n++;
    return n;//没有额外空间
}
int main()
{
	int& r1=Count1();//error:r1想成为临时变量的别名,因为临时变量具有常性。所以不行。需要加const
    int& r2=Count2();//tmp相当于n的别名。r2相当于tmp的别名。
    return 0;
}

image-20210828194342670

image-20210828194442954

6.5传值,传引用的效率比较

#include<ctime>
struct A{
    int a[10000];
};
A a;
A TestFunc1() {return a;}
A& TestFunc2() { return a;}

void main()
{
	size_t begin1=clock();
    for(size_t i=0;i<10000;i++)
    {
        TestFunc1();
    }
    size_t end1=clock();
    cout<<end1-begin1<<endl;
    size_t begin2=clock();
    for(size_t i=0;i<10000;i++)
    {
        TestFunc2();
    }
    size_t end2=clock();
    cout<<end2-begin2<<endl;
}
#include<ctime>
struct A{
    int a[10000];
};
void TestFunc1(A a) {return a;}
void TestFunc2(A& a) { return a;}

void main()
{
    A a;
    //以值作为函数参数
	size_t begin1=clock();
    for(size_t i=0;i<10000;i++)
    {
        TestFunc1(a);
    }
    size_t end1=clock();
    
    //以引用作为函数参数
    size_t begin2=clock();
    for(size_t i=0;i<10000;i++)
    {
        TestFunc2(a);
    }
    size_t end2=clock();
}

总结一下:引用的作用主要体现在传参和传返回值。

  1. 引用传参和传返回值,有些场景下面,可以提高性能。(大对象+深拷贝对象)
  2. 引用传参和传返回值,输出型参数和输出型返回值。通俗点说,有些场景下面,形参的改变可以改变实参。

有些场景下面,引用返回,可以改变返回对象。

6.6引用和指针的区别

在语法层面:指针和引用是完全不同的概念

  • 指针是开空间,存储变量地址
  • 引用是不开空间,仅仅对变量取别名,没有独立空间,和其引用实体共享一个空间。
  • 因此用的时候不要想底层汇编如何实现,只考虑语法层

在底层实现(看反汇编)上,是和指针一样的。

引用和指针的不同点

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但是有NULL指针。
  5. 在sizeof中含义不同:引用的结果是引用类型的大小,指针的结果是地址的大小。
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式应用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全
  9. image-20210830195308689
#include<iostream>
using namespace std;
int main()
{
	int a=10;
    int& b=a;
    
    int* p =&a;
    
    return 0;
}
00000000000007aa <main>:
 7aa:	55                   	push   %rbp
 7ab:	48 89 e5             	mov    %rsp,%rbp
 7ae:	48 83 ec 20          	sub    $0x20,%rsp
 7b2:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 7b9:	00 00 
 7bb:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 7bf:	31 c0                	xor    %eax,%eax
     
     
 7c1:	c7 45 e4 0a 00 00 00 	movl   $0xa,-0x1c(%rbp)
     
     
 7c8:	48 8d 45 e4          	lea    -0x1c(%rbp),%rax
 7cc:	48 89 45 e8          	mov    %rax,-0x18(%rbp)
     
 7d0:	48 8d 45 e4          	lea    -0x1c(%rbp),%rax
 7d4:	48 89 45 f0          	mov    %rax,-0x10(%rbp)
     
 7d8:	b8 00 00 00 00       	mov    $0x0,%eax
 7dd:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
    
     
 7e1:	64 48 33 14 25 28 00 	xor    %fs:0x28,%rdx
 7e8:	00 00 
 7ea:	74 05                	je     7f1 <main+0x47>
 7ec:	e8 7f fe ff ff       	callq  670 <__stack_chk_fail@plt>
 7f1:	c9                   	leaveq 
 7f2:	c3                   	retq   

7.内联函数

VS的c/c++中的优化,内联函数扩展,调成只适用于__inline,在debug下就能展开。

7.1内联函数的概念

内联函数的本质是消除函数调用。

一般情况下在Debug下不能展开。但是Release看不到。

需要VS设置。通过反汇编就可以看到没有Call了。

int Add(int left,int right)
{
  return left+right;  
}
void Swap(int &x1,int &x2)
{
    int tmp=x1;
    x1 =x2;
    x2 =tmp;
}
///频繁调用Swap是有栈帧消耗的
//C语言如何解决:1.C语言使用宏函数(提前展开了)2.C++使用内联函数(会在调用的地方展开)
int main()
{
    int ret=Add(1,2);
}

image-20210830195203676

7.2内联函数的特性

  1. inline函数是一种空间换时间的做法,省去调用函数额外开销。
    1. Call Swap 假设程序中调用了1w次。假设swap10行指令。此时是10010
    2. inline之后就没有调用的call。但是展开后指令个数是100000了。
    3. 一般内联适用于小函数,小于20行。其次递归,长的代码不适用于内联。
  2. inline对于编译器而言只是一个建议,编译器会自动优化。
  3. 内联不建议声明和定义分离,分离会导致连接错误。因为inline被展开,就没有函数地址了,链接就找不到。

7.3相关题目

  • 宏的优缺点
    • 优点:
      • 增强代码的复用性
      • 提高性能
    • 缺点
      • 不方便调试宏(因为预编译阶段进行了替换)
      • 导致代码可读性差,可维护性差,容易误用
      • 没有类型安全的检查
  • C++的替代
    • 用const替换常量定义
    • 短小函数定义换用内联函数

8.auto关键字(C++11)

8.1auto的历史

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。auto修饰的变量表示该变量在函数栈帧结束时释放。但是这是自动的。因此C++11给修改了。

8.2typedid(x).name()查看变量的类型

int main()
{
    int a=10;
    auto b=a;//b的类型是根据a的类型推导出是int
    auto c=a;
    auto d='A';
    auto e=10.11;
}

观察类型的函数:typeid(a).name()

	int a=0;
	auto b=a;
	int& c=a;
	auto& d=a;
	auto* e=&a;//int*
	auto f=&a;//int*
	cout<<typeid(a).name()<<endl;//int
	cout<<typeid(b).name()<<endl;//int
	cout<<typeid(c).name()<<endl;//int
	cout<<typeid(d).name()<<endl;//int
	cout<<typeid(e).name()<<endl;//int*
	cout<<typeid(f).name()<<endl;//int*

8.3auto的使用细则

  1. auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main()
{
 	int x = 10;
 	auto a = &x;
 	auto* b = &x;
 	auto& c = x;
 	cout << typeid(a).name() << endl;
 	cout << typeid(b).name() << endl;
 	cout << typeid(c).name() << endl;
	*a = 20;
 	*b = 30;
 	c = 40;
 	return 0
}
  1. auto推导const变量会自动丢弃const,如果想要const使用,使用const auto
int main()
{
    const int x = 10;
    auto y=x;
    y= 20; 
    cout<<x<<endl;
    cout<<y<<endl;
    const auto z = x;
    z=30;//error;
}

8.4auto不能推导的场景

  1. auto不能作为形参类型,所以不能作为传参

    1. void test(auto a)
      {
          
      }
      
  2. auto不能直接用来声明数组

    1. void test()
      {
          int a[]={1,2,3};
          auto b[] ={4,5,6};
      }
      
  3. 为了避免和C98的auto发生混淆,C11只保留了auto作为类型指示符的用法

  4. auto在实际中最常用的优势用法就是新式for循环和lamabda表达式

9.基于auto的范围for(C++11)

9.1范围for的基本使用

若想通过该方式进行修改元素内容,使用auto声明引用类型时则必须加&

int main()
{
    int array[]={1,2,3,4,5};
    for(int i=0;i<sizeof(array)/sizeof(int);++i)
    {
        array[i]*=2;
    }
    for(int i=0;i<sizeof(array)/sizeof(int);++i){
        cout<<array[i]<<" ";
    }
    cout<<endl;

	//c++11 -->范围for-->特点:写起来比较简洁
    for(auto &e:array)//实际上是把array的值取出来赋值给e。所以要修改要引用。
    {
        e*=2;
    }
    for(auto e:array)
    {
        cout<<e<<" ";
    }
    cout<<endl;
}

9.2范围for的注意事项

当传参数组的时候,数组会退化成指针

void TestFor(int array[])//还记得C基础吗,传数组名就是数组元素首地址,此时退化成了指针。
{
    for(auto& e:array)//error:此刻的array不是数组
    {
        cout<<e<<endl;
    }
}

10.指针空值nullptr

10.1NULL和nullptr的特殊场景

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

C++98/03中#define NULL 0 。

void fun(int n)
{
    cout<<"整型"<<endl;   
}
void fun(int* p)
{
    cout<"整形指针"<<endl;
}
int main()
{
    //C++98/03
    int* p1=NULL;
    int* p3=0;
    
    //C++11,推荐像下面这样去用
    int* p2=nullptr;
    
    fun(0);
    fun(NULL);//跳转的是第一个fun. fun(0)
    fun(nullptr);// fun( (void*)0 )
}
;