c++基础(2)

181 阅读16分钟

函数重载

什么是函数重载

c++允许在同一作用域中定义多个 名字相同 的函数.

比如定义多个Swap函数用于交换整数、浮点数或字符.

编译器可以根据传参的不同来调用对应的函数.

#include <iostream>
using namespace std;

void Swap(int* a, int* b)
{
	cout << "交换整数" << endl;
}
void Swap(double* a, double* b)
{
	cout << "交换浮点数" << endl;
}
void Swap(char* a, char* b)
{
	cout << "交换字符" << endl;
}
int main()
{
	int a1 = 0;
	int a2 = 1;
	double a3 = 1.5;
	double a4 = 6.6;
	char a5 = 'b';
	char a6 = 'c';
	Swap(&a1, &a2);
	Swap(&a3, &a4);
	Swap(&a5, &a6);
	return 0;
}

image.png

三个好处:

1 可以处理实现功能类似,数据类型不同的问题.

2 不用为取名发愁.例如c语言就要用swapi(交换int型的函数),swapd(交换double型的函数);

3 使用的地方更方便,就像用同一个函数一样.

构成函数重载的条件

条件1: 在同一作用域中定义的多个同名函数.

不同作用域间的同名函数之间不构成函数重载.(参考命名空间)

条件2:同名函数的 参数类型 或 参数类型的顺序 或 参数数目 不同.

(条件2里的任意情况发生即可)

void Swap(int* a,double* b)
{}
void Swap(double* a,double* b)//参数类型不同(有一个类型不同即可)
{}


void Swap(char* a, char* b,int* c)
{}
void Swap(char* a, char* b)//参数数目不同
{}


void Swap(char* a, double* b)
{}
void Swap(double* a, char* b)//参数类型相同,参数的顺序不同
{}
//以上6个Swap函数相互之间构成函数重载

注意:只有返回值不同的同名函数不能构成函数重载,

因为调用函数时我们只会传参,编译器只能根据参数来判断是调用哪个函数.

c/c++程序编译链接的基本情况

可以先看我之前的一篇文章:

C程序的编译和链接初步讲解 - 掘金 (juejin.cn)

在C/C++中,程序要运行,要经历4个阶段(分细):

预处理/预编译、编译、汇编、链接.

以以下三个文件为例:

test.h

image.png

test1.cpp

image.png

test2.cpp

image.png

预处理/预编译主要工作:

头文件展开、宏替换、条件编译、去掉注释.

test1.cpp->test1.i; test2.cpp->test2.i

此时参加编译的就只有test1.i和test2.i,因为test.h已经在这两个文件展开了.

编译主要工作:

1 词法分析、语义分析、语义分析、符号汇总(将出现的全局变量名,函数名梳理出来).

2 将c语言代码翻译成汇编代码.

注意:文件只会汇总自己文件内部的符号.

test1.i ->test1.s;test2.i -> test2.s

test1.s 里会汇总print1 和print2.

test2.s 里会汇总print1、print2、printf.

汇编主要工作:

1 将汇编指令翻译成二进制指令,生成目标文件(Linux下.o文件,Window下.obj文件).

2 给每个文件形成一个符号表,将汇总的符号填进去,同时赋一个有效或无效的地址.

(比如这个文件中只有函数的声明,就赋无效地址;有函数定义,就直接添上函数地址)

test1.o:

print1无效地址
print2无效地址

test2.o:

print1有效
print2有效
printf无效地址

链接主要工作:

1 合并目标文件.

2 符号表的合并和重定位.(将所有文件的符号表合并,将无效地址覆盖

如果还存在无效地址,说明有函数或全局变量只有声明没有定义.

3 链接外部的库和C语言库,比如库里有printf的实现代码(二进制),

相当于库里才有printf的地址/具体实现.

4 生成可执行程序.

c++如何实现函数重载的(重点)

关键在于编译时汇总的符号.

C语言为什么不支持函数重载?

因为C语言放在符号表里的函数名就单单只是函数的名字!

如果定义了两个同名的函数(C语言只有全局域或局部域),

那符号表中就会出现相同符号、(多个有效地址)地址不同的情况,这显然自相矛盾.

C++如何支持函数重载?

C++为了支持函数重载,引入了函数名修饰规则,即把参数类型作为后缀来修饰函数名.

这样符号表里的符号实际上是函数名+参数类型后缀.

C++程序即使函数名相同,但参数不同,也能在符号表中区分不同的函数,找到函数的正确地址.

image.png

_Z5printic:_Z是前缀,5表示真正函数名print的长度,

i即第一个参数是int类型,c即第二个参数是char类型,ic是后缀.

//test.h
#include <stdio.h>
void print(int a1,char a2);
void print(char a1,int a2);

//test1.cpp
#include "test.h"
int main()
{
  print(1,'a');
  print('b',2);
  return 0;
}

//test2.cpp
#include "test.h"
void print(int a1, char a2)
{
   printf("%d %c\n",a1,a2);
}

void print(char a1, int a2)
{
  printf("%c %d\n",a1,a2);
}

引用

引用的概念

给已有的变量取别名/外号.

编译器不会为引用变量开辟内存空间.

它和它引用的变量共用一块空间.

类型& 引用变量名 = 引用实体

#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int& b = a;//此时b是a的别名
	cout << "a的地址是:" << &a << endl;
	cout << "b的地址是:" << &b << endl;
	return 0;
}

image.png

注意:&单独跟变量时才是取地址,而上面是引用.

此时修改a也就是修改b,因为a和b共用一块内存空间.

引用的使用细节

1 引用在定义时必须初始化,指定要引用的变量.

image.png

image.png

2 一个变量可以有多个引用

就像人可以有多个外号一样

也可以给别名取别名

#include <iostream>
using namespace std;
int main()
{
    int a = 0;
    int& b = a;
    int& c = a;
    int& d = b;//给别名取别名

    cout << "a的地址是:" << &a << endl;
    cout << "b的地址是:" << &b << endl;
    cout << "c的地址是: " << &c << endl;
    cout << "d的地址是:" << &d << endl;
    return 0;
 }

image.png

3 引用一旦引用一个实体,就不能再引用其它实体

引用的使用场景

函数参数

1 做输出型参数

平时的参数是输入型参数,形参是实参的临时拷贝,形参的改变不影响外面任何值.

而输出型参数,是能通过形参影响外面的值.

以交换函数为例:

A 普通的函数参数

a、b的交换不会影响实参,交换失败

void swap(int a, int b)//形参是实参的临时拷贝
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int c = 1;
    int d = 2;
    swap(c, d);
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;
    return 0;
 }

B 引用做函数参数

此时形参是实参的别名,形参的交换就是实参的交换.

形参的改变就是实参的改变.

void swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int c = 1;
    int d = 2;
    swap(c, d);
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;
    return 0;
 }

2 大对象传参,可以提高效率

因为平时的传参,形参是实参的临时拷贝.

若实参的类型是很大的结构体类型,拷贝会有不少消耗.

而引用做形参,可以减少拷贝,提高效率.

做返回值

1 传值返回

int func()
{
    int a = 0;
    return a;
}
int main()
{
    int ret = func();
    cout << ret << endl;
    return 0;
}

问题:func()是用a进行返回吗?

编译器不会用返回对象作为函数的返回值,

而是先生成一个拷贝的临时对象.

即拷贝a后,再拿这个拷贝做函数的返回值,再把这个值给ret.

问题:编译器为什么要这么做?

image.png

函数调用完成后是先销毁栈帧,再进行返回

在返回之前,这块空间(栈帧)已经被销毁了,a的值自然也是不确定的

因此要生成临时对象作为函数的返回值.

问题:临时对象在哪里?

若返回对象很小,临时对象一般存放在寄存器中.

否则,就会在调用方的栈帧里提前在一个位置开好,作为临时对象的位置;

即在栈帧销毁之前,把它拷贝到临时对象的空间上,临时对象再做func()的返回值.

问题:如果返回对象在函数栈帧销毁后不销毁,比如存放在静态区,仍然生成临时对象做返回值吗?

编译器只看是不是传值返回,只要是传值返回,就会生成一个拷贝的临时对象作为函数的返回值.

传值返回总结:

会生成一个返回对象的拷贝作为函数调用的返回值.

若返回对象很小,那临时变量存放在寄存器中;

否则,栈帧销毁前,会在调用方的栈帧中开好一块空间,将返回对象的值拷贝上去.

这个临时对象做函数的返回值.

2 传引用返回

int& func()
{
    static int a = 0;//a保存在静态区,不在栈区
    return a;
}
int main()
{
    int ret = func();
    cout << ret << endl;
    return 0;
 }

问题:什么是传引用返回?

函数调用完成后,返回返回对象的别名(也是临时对象),但不再生成拷贝.

问题:什么时候使用传引用返回?

当函数栈帧销毁后(出了函数作用域后),返回对象仍然没有销毁.

比如返回对象在静态区、堆区.

错误使用1:

int& func()
{
    int a = 0;
    return a;
}
int main()
{
    int ret = func();
    printf("%d\n", ret);
    return 0;
 }

函数栈帧销毁后,返回对象a也已经销毁,

但由于传引用返回,就会返回a这块空间的别名(假设返回的别名叫tmp).

要将返回值给ret,就相当于要去a这块被销毁的空间上取值,

越界访问,结果是没有保障的.

image.png

问题:为什么这里的运行结果却是正确的

我们到酒店去订房,把手机放在房间里,理论上是安全的,手机不会丢;

然而当把房间退了,手机被遗漏在房间里,过几天你偷进到这个房间去找,

你能保证你一定能找到这部手机吗?

如果能找到,说明工作人员在你退房后还没有清理这个房间,

也没有其它人占了这个房间,纯属侥幸.

如果不能找到,说明被其它人带走了………………

因此,建立函数栈帧就像申请房间一样,在里面放一个变量,

理论上不会自动被修改,但栈帧一旦被销毁:

系统如果会清理栈帧置成随机值,那么这里的ret取到的是随机值;

暂时不会清理就还是原来的值;

但越界访问是没有保障的.

错误使用2:

int& func()
{
    int a = 0;
    return a;
}
int main()
{
    int ret = func();
    printf("%d\n", ret);
    printf("%d\n", ret);
    return 0;
 }

image.png

函数调用完成后,返回返回对象a的别名,所以会去访问一次a的空间,

把这块空间的值赋给ret,之后ret的值就是固定的.

上例的函数栈帧销毁后并没有立刻清理栈帧,所以值是正确的.

错误使用3:

int& func()
{
    int a = 0;
    return a;
}
int main()
{
    int& ret = func();//如果ret也是引用呢?
    printf("%d\n", ret);
    printf("%d\n", ret);
    return 0;
 }

image.png

func()返回返回对象a的别名,而ret却也是引用,间接导致ret是a这块空间的别名,

而此时a这块空间已经销毁了(不属于用户申请的空间,没有使用权).

image.png

问题:为什么第一次打印是正确的,第二次打印却是随机值?

调用printf()时,要先传参,取ret的值传给printf().

所以在调用printf()时,先要去a的空间里取值,此时这个空间还没有被清理.

把参数传给printf()后,此时传的参数仍是未清理的值,接着建立printf()的栈帧

此时这块空间会覆盖掉原来的func()的栈帧,导致a的空间被置成随机值.

第二次调用printf(),继续去a的空间上取值给printf()传参,打印出来的值是随机值.

image.png

传引用返回总结

出了函数作用域或者函数栈帧销毁后:

返回对象会销毁 —— 不能使用传引用返回,一定要用传值返回.

不会销毁 —— 返回返回对象的别名,减少拷贝,提高效率.

常引用

权限问题

c++中,const修饰的变量名是直接视为常量,可读不可写.

    int a = 0;//可读可修改
    const int b = 0;//可读不可修改
    b = 2;//会报错

image.png

权限的平移

b是a的引用:

a的权限是可读可修改的,

b的权限也是可读可修改的.

d是c的引用:

c的权限是只读的,

d的权限也是只读的.

    int a = 0;
    int& b = a;
    const int c = 1;
    const int& d = c;

权限的放大

编译会报错.

b是a的引用:

a的权限是只读的,

b的权限却是可读可写的.

    const int a = 0;
    int& b = a;

image.png

权限的缩小

权限较小的b引用了权限较大的a,编译成功.

    int a = 0;
    const int& b = a;

因为b是a的别名,所以它们共用一块内存空间.

但使用b无法对这块空间进行修改,而用a可以修改.

int main()
{
    int a = 0;
    const int& b = a;
    cout << b<<endl;
    a = 10;
    cout << b;
    return 0;
}

image.png

总结

权限不能放大,但可以缩小.

类型转换生成的临时变量

隐式类型转换和强制类型转换中间会产生临时变量

image.png

为什么中间会生成临时变量?

char类型赋值给int类型会发生隐式类型转化,但为了不改变ch的类型,

需要中间产生临时变量,将ch先赋给临时变量,按提升的规则去补位,

提升完的值再赋给i;

那不同类型进行引用会发生什么?

这里不同类型进行引用失败了.

image.png

可是引用变量的类型加const修饰,编译成功了?

image.png

隐式类型转换中间会生成临时变量,不同类型之间进行引用也会生成临时变量,

但该临时变量具有常性,b引用的实际上是该临时变量,

但由于临时变量是只读的,所以导致权限的放大.

强制类型转换也会生成临时变量,引用变量也要加const才能成功引用:

image.png

总结

一开始不能引用的本质原因是:引用的是临时变量,临时变量具有常性,导致权限的放大.

const引用的使用

const引用还可以直接引用一个常量.

const double& b = 1.1;

const引用还可以接受要进行隐式类型转换的参数,直接接受常量,

可见const引用有很强的接收度,作为函数参数时能接收各种传过来的实参

void func(int& a){}
int main()
{
    int a = 0;
    double b = 1.2;
    const int c = 2;
    func(a);
    //错误,这下面传参导致权限的放大,导致编译错误
    func(b);
    func(c);
    return 0;
 }

如果把形参改为const int& a,这些参数全都能传递过去.

总结

1 引用做参数时,若参数不会在函数体内被改变,建议加上const,否则有些参数可能传不进去.

2 传值传参不涉及权限放大缩小问题,因为仅仅是拷贝(实参拷贝给形参).

指针和引用的区别

使用场景

指针和引用的使用场景是很相似的,

做函数参数:都可以做输出型参数;大对象传参,提高效率.

做函数返回值:减少拷贝,提高效率.

但指针有一个引用无法替代的场景:链式结构.

struct ListNode
{
    struct ListNode* next;
    int val;
};

1 指针有NULL,对于尾结点的next可以置空,而引用必须在定义的时候初始化;

2 引用一旦引用了一个实体,就不能引用其它实体,当往链表插入一个新结点,

如果是指针next可以直接改指向,而引用不能;

语法细节

从使用场景里已经有两个不同点.

3 指针有一级指针、二级指针…………而引用只是给变量取别名.

4 sizeof(指针)永远是4/8byte,sizeof(引用)永远是被引用类型的大小.

5 指针+1是向后偏移指向的类型大小,引用变量+1是引用实体大小+1.

6 访问实体方式不同,指针需要解引用,由于引用变量就是别名所以不需要解引用*.

底层原理

7 语法角度而言:引用没有开空间,指针开了4byte/8byte;

底层实现的角度:引用其实是用指针实现的.

使用引用的汇编代码: image.png

使用指针的汇编代码: image.png

其它

内联函数

什么是内联函数?

以inline修饰的函数叫内联函数.

编译时c++编译器会在调用内联函数的地方展开内联函数体的代码,

没有建立栈帧的开销.

inline void swap(int& a, int& b)
{}

内联函数的意义

短小的函数(只有1~5行左右),频繁调用,多次建立栈帧,效率不高.

场景:在堆排序中,要排10W个数据,里面的Swap()要调用10W次.

此时把Swap()设置为内联能有效提高效率,避免多次建立栈帧的消耗.

内联函数的特性

1 编译时,编译器会在调用内联函数的地方展开函数体里的代码,进行编译.

代码的指令长度会增大,inline是一种以空间换时间的做法.

因此当代码很长或有递归时不适合作为内联函数.

image.png

2 inline对编译器仅仅只是建议,它会自动优化,

如果函数内有递归或函数内部实现代码的指令长度很长时,会忽略掉内联.

即加了inline不一定会去展开.

3 inline不能声明和定义分离,分离会导致链接错误(inline放声明里)

在test.cpp文件中只有声明,在文件里调用该函数.

由于只有声明,没办法展开,只能去call函数地址,

实现为内联的函数名不会被放到符号表中,就会导致链接错误.

建议:内联函数的实现直接放到头文件中.

auto关键字与for循环的新用法(c++11)

auto关键字(c++11)

可用于自动推导变量

struct ListNode
{
    struct ListNode* next;
    int val;
};
int main()
{
    struct ListNode a;
    a.next = NULL;
    a.val = 0;
    auto x = a;//自动推导x的类型为struct ListNode
    cout << x.next << " " << x.val << endl;
    cout << "a的地址是:" << &a << endl;
    cout << "x的地址是:" << &x << endl;
    return 0;
}

image.png

auto应用场景

1 如果类型写起来比较长,可以考虑用auto自动推导.

如上述x的类型为struct ListNode,用auto自动推导会更方便.

2 与for循环的新用法结合

遍历数组

    int arr[] = { 1, 2, 3, 4, 5, 6 };
    for (auto x : arr)
    {
        cout << x;
    }

image.png

auto —— 自动推导类型.

auto x : arr —— 自动依次取arr里面的每个数据的值,依次赋值给x.

自动迭代,自动++,自动判断结束.

不管数组里面的值是什么类型,auto x都能自动推导.

但是x只是数组里面每个数据的拷贝,x的改变不会影响数组里的值.

3 如果想用上述遍历方式修改数组里的值,使用auto& x:arr.

    int arr[] = { 1, 2, 3, 4, 5, 6 };
    for (auto& x : arr)
    {
        x++;
        cout << x;
    }

image.png auto& x : arr —— 遍历数组arr时,x依次是数组中每个数据的别名,修改x也能修改数组里的值.

auto注意事项

1 在同一行声明多个变量时,必须是相同类型,否则编译器报错.

    auto a = 3, b = 4;
    
    auto c = 3.3, d = 4.4;

    //auto e = 7, f = 1.2;这里会报错

2 auto不能作为函数形式参数的类型.

image.png

3 auto不能用来声明数组.

image.png

4 auto* 、auto、auto&区别

    int a = 1;
    auto b = a;//int类型

    auto c = &a;//int*类型
    auto* d = &a;//int*类型

    auto& e = a;//int类型

    cout << typeid(b).name() << endl;//用来打印变量的类型
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;
    cout << typeid(e).name() << endl;

image.png

auto* d:显式写一个*,表示d一定是一个指针类型,强调一定要用指针初始化d,否则报错.

auto& e:显示写一个&,表示e是一个引用.

5 auto 变量必须在定义时初始化,否则不知道这个变量是什么类型.

指针空值nullptr(c++11)

在c++中,NULL是宏,可能被定义为0,或者被定义为(void*)0.

但编译器默认情况下会将它看成是一个整型常量0.

void func(int* a)
{
    cout << "int*" << endl;
}

void func(int a)
{
    cout << "int" << endl;
}
int main()
{
    int a = 0;
    int* p = NULL;
    func(NULL);//本应该调用func(int*a)
    func(p);
    func(&a);
    return 0;
}

image.png

于是c++11引入了关键字nullptr,用于替代NULL.

c++11建议空指针统一用nullptr.