当前位置:   article > 正文

硬核两万字文章带你C++入门

硬核两万字文章带你c++入门

C++入门

C++关键字

C语言关健字32个,C++关键字63个关键字

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

C++是在C的基础上发展起来的,C++是兼容C的大多数的语法

命名空间

C++是怎么输出hello world的呢?和C语言有什么区别呢?

#include<iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

image-20210924222743007

我们创建一个C++文件后,写出上面代码就会在屏幕上打印hello world,那么上面代码中的using namespace std;是什么意思呢?这就是我们要讲的命名空间。

C++增加命名空间是为了解决C语言的不足,在一段C语言代码中,我们编译下面的代码会报错:

#include<stdio.h>
#include<stdlib.h>
int rand =10;
int max=10;
int main()
{
    printf("%d\n",rand);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image-20210924223143111

那么为什么会报错呢?是因为我们包含了stdlib.h这个头文件

这是一个命名重定义的问题,在我们编写C语言代码时,我们定义变量名时,可能与库函数中的命名冲突了,实际大型项目开发,还存在同事之间定义的变量、函数、类型命名冲突等等

所以为了弥补C语言的不足,C++提出了命名空间来解决命名冲突的问题

那么命名空间是怎么定义的呢?

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

#include<stdio.h>
#include<stdlib.h>
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int rand = 10;
}
int main()
{
    printf("%d\n",Z::rand);//::域作用限定符
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

::是域作用限定符,通过这个我们可以找到命名空间中的定义的变量或者其他东西,这样我们就可以定义rand,并可以成功的打印它

image-20210924224726053

我们也可以在里面定义函数:

#include<stdio.h>
#include<stdlib.h>
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int Add(int left,int right)
    {
        return left+right;
    }
}
int main()
{
    Add(1,2);//直接调用他找不到,因为他只会在全局里面找,不会去域里面
    Z::Add(1,2);
    return 0;
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里需要注意的是直接调用他找不到,因为他只会在全局里面找,不会去域里面,所以我们需要利用域作用限定符去调用Add函数

命名空间还可以嵌套定义:

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
    namespace S//会将外面的定义的变量隔离开来,定义了一个命名空间域
    {
        int Sub(int left,int right)
        {
            return left-right;
        }
    }
}
int main()
{
    Add(1,2);//直接调用他找不到,因为他只会在全局里面找,不会去域里面
    Z::Add(1,2);
    Z::S::Sub(1,2);
    return 0;
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们在调用嵌套的命名空间的函数时,比如我们现在调用Sub函数,我们需要用Z::S::Sub(1,2);去调用该函数

在一项工程当中不同文件的命名空间的定义名字可以相同,并且该命名空间中的定义编译器会合并在一起,比如:

在Add.cpp这个文件中我们定义了一个命名空间

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
}
  • 1
  • 2
  • 3
  • 4
  • 5

在Add.h中我们定义了命名空间

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int Add(int left,int right)
    {
        return left+right;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

编译器会将这两个文件中定义的空间进行合并:

namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

那么我们如何使用命名空间的东西呢?

有三种方式:

  • 1、全部直接展开到全局
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}
using namespace Z;//将Z命名空间展开
int main()
{
    Add(1,2);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

优点是用起来方便。缺点是把自己的定义暴露出去了,导致命名污染

using namespace std;//std是包含C++标准库的命名空间
  • 1

std是包含C++标准库的命名空间,这里其实就将C++标准库展开了,这就解释了我们开头打印hello world时,前面为什么有一个using namespace std;这里就很好的解释了这个代码的意思

  • 2、访问每个命名空间中的东西时,指定命名空间
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域
{
    //定义变量、函数、类型
    int rand = 10;
    int Add(int left,int right)
    {
        return left+right;
    }
}
int main()
{
    Z::Add(1,2);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

优点:不存在命名污染。缺点:用起来麻烦,每个都得去指定命名空间

  • 3、将命名空间中常用的展开
namespace Z//会将外面的定义的变量隔离开来,定义了一个命名空间域,名字叫Z
{
    //可以定义变量、函数、类型
    int rand = 10;
    int Add(int left, int right)
    {
        return left + right;
    }
}
int main()
{
    using Z::Add;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

不会造成大面积的污染,也可以解决每个都指定命名空间的问题,这是一个将1和2折中的解决方案


C++输入&输出

这又回到了我们开始提到的C++是怎么输出hello world到屏幕上的,我们来看下C++是如何来实现的:

#include<iostream>
//展开常用的
using std::cout;
using std::endl;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

按照上面我们所讲的,std是一个命名空间,命名空间中有我们要使用的cout和endl,所以我们这里只将常用的展开,但是在日常学习中,我们并不需要像项目中那么规范,我们可以直接将std命名空间展开。

#include<iostream>
//展开常用的
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们了解了输出,那么C++是如何进行输入的呢?我们使用cin标准输入,看下面代码:

#include<iostream>
//展开常用的
using namespace std;
int main()
{
    int n;
    cin >> n;//>>输入运算符 流提取运算符
    int* n = (int*)malloc(sizeof(int)*n);
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    for(int i=0;i<n;i++)
    {
    	cout << a[i]<<" ";//<<输出运算符/流插入运算符  可以连续输出
    }
    cout<<endl;
    //等价于
    cout<<"\n";
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

image-20210924231820389

注意:使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。

我们在输出时可以连续输出,cout<<endl;实际上就等价于cout<<"\n"。

另外C语言输入输出时需要指定类型的,C++不用指定类型可以自动识别类型:

double* n = (double*)malloc(sizeof(double)*n);
for(int i=0;i<n;i++)
{
    cin>>a[i];//自动识别类型
}
  • 1
  • 2
  • 3
  • 4
  • 5
int main()
{
    int i=2;
    double d = 1.111;
    int* pi = &i;
    cout<<i<<endl
    cout<<d<<endl
    cout<<pi<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image-20210924232142152

在写C语言printf和scanf函数时,前面我们需要指定输入输出的格式,而C++则是自动识别类型。

而在下面的场景中,用printf会更好一点

struct Student
{
	char name[10];
	int age;
};
int main()
{
	struct Student s = { "张三", 18 };
	cout << "名字:" << s.name << " " << "年龄:" << s.age << endl;
	printf("名字:%s 年龄:%d\n", s.name, s.age);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

缺省参数

缺省参数概念

所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就不需要传参。可以传参数,也可以不传,如果不传,函数参数用缺省的

我们看下面代码:

#include<iostream>
using namespace std;
void TestFunc(int a = 0)//参数缺省值
{
    cout<<a<<endl;
}
int main()
{
    TestFunc();//不传会用默认值
    TestFunc(10);//传了就缺省参数就没用了
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image-20210924232602375

我们可以看到没有传参的使用了缺省值,而传参的就忽略缺省值了,只考虑传过来的参数

那么缺省参数有什么用呢?

我们在数据结构中,设计栈这个数据结构时:

void StackInit(struct Stack* ps, int defaultCP)
// 假设我明确知道这里至少要存100个数据到st1里面去
struct Stack st1; 
StackInit(&st1, 100);
  • 1
  • 2
  • 3
  • 4

如果我们假设明确知道一个栈st1最少使用100个空间,我们给栈的容量初始化为100,所以这时我们在栈初始化这个接口引入一个参数defaultCP,我们想要弄100个容量,我们直接传参100就可以了。

但是当我们不能明确知道栈st2的需要使用的空间,如果上面不设置缺省参数,那么我们必须要传一个值进去,那我们传的值大了的话,会造成空间浪费,传的小了的话会不够用,那么此时这个缺省参数就派上用场了,我们这样定义:

void StackInit(struct Stack* ps, int defaultCP = 4)
struct Stack st2; 
StackInit(&st2);
  • 1
  • 2
  • 3

不传值时默认它为4,不够了再进行扩容就可以了


缺省参数分类

全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

全缺省参数顾名思义就是全部都有缺省参数,我们在传参时不能间隔着传,比如:

TestFunc(100, ,50);//这样是错误的
  • 1

编译器是不允许这样进行传参的

半缺省参数
void TestFunc(int a, int b = 10, int c = 20)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
int main()
{
    //没有缺省的必须传参
    //半缺省参数必须从右往左依次来给出,不能间隔着给
    TestFunc(1);
    TestFunc(1,2);
    TestFunc(1,2,3);
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意:

  • 没有缺省的必须传参

比如这上面的a参数必须要传参

  • 半缺省参数必须从右往左依次来给出,不能间隔着给

比如:

void TestFunc(int a = 30, int b = 10, int c)
  • 1

这样就是错误的,没有按照从右往左的顺序

也不能这样:

void TestFunc(int a = 30, int b, int c = 10)
  • 1
  • 缺省参数不能在函数声明和定义中同时出现
//a.h
void TestFunc(int a = 10);
//a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

  • 缺省值必须是常量或者全局变量
void TestFunc(int a = x);//error
  • 1
  • C语言不支持(编译器不支持)

C语言是不支持缺省参数的。


函数重载

函数重载概念

重载函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者类型顺序)必须不同,也就是说用同一个函数完成不同的功能。

//函数重载
int Add(int left, int right)
{
	return left+right;
}
double Add(double left, double right)
{
	return left+right;
}
long Add(long left, long right)
{
	return left+right;
}
int main()
{
    Add(10,20);//会调用对应参数类型的函数
    Add(10.0,20.0);
    Add(10L,20L);
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

函数重载面试:为什么C++支持函数重载,而C语言不支持?

C编译器,直接用函数名关联,当函数名相同时,它无法区分

C++如何支持重载呢?

函数名修饰规则:不能直接用函数名对函数进行修饰,代入参数特点修饰,函数名相同,只要参数不同,修饰出来的名字就不同,就可以区分两个函数了,就支持重载了,把定义在文件中的函数调用,地址找到(符号表里找),链接在一起,所有.o文件进行合并,生成一个执行文件。

函数重载:

要求参数不同,因为参数不同修饰出来的名字就不同

编译器能不能实现函数名相同参数相同返回值不同,就能构成重载?

不行

int func();//-> _Z4ifunc
double func();//-> _Z4dfunc
  • 1
  • 2

如果把返回值带进修饰规则,那么编译器层面是可以区分的。但是语法调用层面,无法区分,带有严重的歧义!

执行func()时;调用时,到底是调用哪个呢?这是不知道的,所以不能


名字修饰

extern"C"

对于extern"C"的讲解我们举一个例子:

C++实现编写成动态库或者静态库,写一个C++的程序去调用这个库是没问题的,但是我们写一个C程序就不行,程序是因为链接时会有问题,比如静态库有一个函数void* tcmalloc(size_t n) (谷歌提供的更高效替代malloc的库)
C程序在链接时,直接用函数名tcmalloc去找函数的地址,但是因为C++有名字修饰,而生成的符号表是:0x662521:_Z8tcmallocui,而C++是用_Z8tcmallocui去找的,C++可以找到,因为该动态库是C++写的,而C语言程序就找不到,因为直接用函数名tcmalloc去找函数的地址,匹配不到

那么有什么方式能让C程序和C++的程序都能用这个C++的库呢?

C++就出现了extern"C",在声明tcmalloc这个函数时在前面加extern"C" void* tcmalloc(size_t n),此时符号表按C语言修饰规则就成为了0x662521:tcmalloc,这时C程序就能正确的找到了,C++程序中有tcmaloc函数的声明,此时它发现前面有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的。

总结:

当C的程序和C++的程序都想调用一个C++实现的模块时,C++可以调,但C语言不能调,那么两个都想调用时,我们就在C++实现的该模块里面函数声明时在前面加extern"C",那么生成的符号表里面就不对这个函数进行修饰了,这时C语言实现的程序就可以调用它了,紧接着C++程序在调用时,因为C++是兼容C的,发现函数的声明有extern"C",所以就在链接的时候按C的规则去找这个模块的函数

面试题:

下面两个函数能不能构成函数重载?

void TestFunc(int a = 10)
{
	cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
	cout<<"void TestFunc(int)"<<endl;
}
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

不行,重载必须要函数参数列表不同,因为这样才在名字修饰时有所区别,能够区分函数。

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

C编译器,直接用函数名关联,当函数名相同时,它无法区分

C++中函数重载底层是怎么处理的?

在链接阶段,有符号表的合并与重定义,那么函数名相同那么他们重载函数符号表会不会冲突呢?答案是不会的,C++有自己的名字修饰规则,比如重载函数参数类型一个为int,另一个为float,在名字修饰时,就会将这些信息代入进去,这样就区分了两个函数。

C++中能否将一个函数按照C的风格来编译?

可以,在该函数前面加extern"C"即可,C++程序中有函数的声明,发现有extern"C",就按C语言修饰规则去找,因为C++兼容C,所有C的修饰规则它也是知道的


引用

引用概念

引用并不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

语法:类型& 引用变量名 = 引用实体;

#include<iostream>
using namespace std;
int main()
{
    int a=0;
    int& ra = a;//ra是a的引用,引用也就是别名,a再取了一个名称ra
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

ra是a的引用,引用也就是别名,a再取了一个名称ra

我们可以重复引用,也可以对本身是引用的变量名再次进行引用,例如:

int main()
{
    int a=1;
    int& ra = a;
    int& b = a;
    int& c = b;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们随便改变一个变量都会变,因为他们共用同一块空间 :

int main()
{
    int a=1;
    int& ra = a;
    int& b = a;
    int& c = b;
    
    b=4;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们可以进行调试观察:

image-20210925170136531

image-20210926195245139

发现四个变量都变成了4,并且他们共用一块空间


引用特性

  • 引用必须在定义的时候初始化
#include<iostream>
using namespace std;
int main()
{
    int a=0;
    int& ra;//error
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

image-20210925170938884

  • 一个变量可以多个引用

这个在前面已经证实了

  • 一个引用有了一个实体就不能有其他实体
int main()
{
    int a =1;
    int& c = a;
    int d =2;
    c=d;//分析:这里是c变成了d的引用呢还是将d赋值给c
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

image-20210925171623078

我们可以看到a和c的地址是一样的,说明c没有变成d的引用,而a变成了2,说明c=d而是将d赋值给c


常引用

我们前面说的引用是这样的:

int main()
{
    int a =0;
	int& b=a;//b的类型是int,而不是int&
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样当然是可以的,但是看下面代码,这样可以吗?

int main()
{
    const int a =0;
	int& b=a;//b的类型是int,编译不通过,原因:a是只读,b的类型是int,也就是可读可写的,所以不行
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

b的类型是int,编译不通过,原因:a是只读,b的类型是int,也就是可读可写的,变成你的别名,还能修改你,所以不行

所以需要这样写:

int main()
{
    const int a =0;
	const int& b=a;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

那么这样写可以吗?

int main()
{
    int a =0;
	const int& b=a;//行不行?
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样写是可以的,a是可读可写的,b变成别名是只读,变成a的别名,不修改你,这样是可以的。但是b是不可以改的(b是const修饰,只读不能写),但是a可以改,并不是每个别名和原名字有一样的权限

总结:

引用取别名时,变量访问的权限可以缩小,不能放大

**那么这个有什么用呢?**比如我们再学习数据结构—栈的实现时:

typedef struct Stack
{
	int a[1000];
	int top;
	int capacity;
}ST;
void StackInit(ST& s) // 这里传引用是为了形参的改变,影响实参
{
	s.top = 0;
	s.capacity = 1000;
	// ...
}
void PrintStack(const ST& s)  
// 1、传引用是为了减少传值传参时的拷贝  
// 2、可以保护形参形参不会被改变
{
}
// 总结:函数传参如果想减少拷贝用了引用传参,如果函数中不改变这个参数
// 最好用const 引用传参
int main()
{
	ST st;
	StackInit(st);
	// ...
	PrintStack(st);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

我们通常是结构体传指针,因为这样可以不进行拷贝,就节省了空间的消耗,我们现在用引用也可以实现,PrintStack这个函数的实现我们不希望修改结构体成员变量,故这就用到了我们前面讲的常引用,在前面加const就不能改变结构体成员变量了,这样可以增加代码的健壮性,假设函数里面有修改结构体成员变量的操作,这里就会被检查报错,总结一下:传引用的好处是什么呢? 1、传引用是为了减少传值传参时的拷贝 2、可以保护形参,形参不会被改变

const引用做参数的第二个好处:

void func(const int& n) // const引用做参数的第二个好处,既可接收变量,也可以接收常量
{

}
int main()
{
    int i = 10;
	func(i);
	func(20);
	func(j);
    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

func该函数既可接收变量,也可以接收常量,const引用在实际当中的作用:函数传参如果想减少拷贝用引用传参,如果函数中不改变这个参数,最好用const引用传参

下面我们再来看常引用的另外的知识,首先我们看下面代码:

int i=0;
double d = i;//隐式类型转化
  • 1
  • 2

这样写是可以的,那么这样写可以嘛?

int i=0;
double &d = i;
float &f = i;
  • 1
  • 2
  • 3

这样是不行的,但是加上const就可以了

int i=0;
const double &d = i;
const float &f = i;
  • 1
  • 2
  • 3

为什么呢?要理解这个我们首先就要看一下这个赋值:

int i=0;
double d = i;//隐式类型转化
  • 1
  • 2

这里的赋值实际上并没有直接把i给d,隐式类型转化实际上中间产生了一个double的临时变量,i给这个临时变量,这个临时变量再给d,不光隐式类型转化产生临时变量,而且整型提升也会产生临时变量

const double &d = i;
const float &f = i;
  • 1
  • 2

这里就可以解释为什么加const可以,不加const不可以了,因为中间会产生一个double的临时变量,它们引用的不是i,而是引用的这个临时变量,而临时变量具有常性,而没有const时,d和f都是可读可写的,而该临时变量是只读的,故将权限放大了,所以不行,加上const,都是只读的,所以是可以的。


引用的使用场景

传参

我们之前是如何编写一个函数交换两个数的呢?我们需要传地址,用指针去接收

void Swap(int* p1,int* p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
int main()
{
    int x=0,y=1;
    Swap(&x,&y);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这是之前我们交换两个数的代码,但是有了引用之后我们可以这样写:

void Swap(int& rx,int& ry)
{
    int temp = rx;
    rx = ry;
    ry = temp;
}
int main()
{
    int x=0,y=1;
    Swap(x,y);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

因为rx是x的引用,ry是y的引用,他们的地址是一样的,操作rx和ry就相当于操作x和y

下面我们来看一个指针变量的引用:

int main()
{
    int x = 0,y = 1;
    int *p1 =&x;
    int *p2 =&y;
    int*& p3 = p1;//p3是p1的别名
    *p3 = 10;
    p3 =p2;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image-20210926204302620

这是前六行代码的图解,那么第7行第8行代码执行后会是什么样子呢?

image-20210926204556796

x变成10,p1和p2、p3都指向了y

返回值

首先我们先看这个代码:

传值返回:

int Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里返回的不是c,是将c拷贝给临时变量,临时变量做返回值

如果比较小,通常是寄存器

如果比较大,会在main中开一块临时空间

int Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    const int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这里返回的不是c,将c拷贝给临时变量,而是临时变量做返回值,而临时变量具有常性,所以接受的ret引用返回值需要加const

下面这段代码会输出什么呢?

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image-20210927150519136

函数Add在返回时,int& temp = c;temp是c的别名,将c的别名temp返回(其实就是n),ret是temp的别名,即ret是c的别名,返回后第一次调用的Add函数栈帧就销毁了,但是这里销毁并不是空间不存在了,而是空间的使用权不是你的了,再次调用时,开辟相同大小的栈帧,c变成了7,又因为ret是c的别名,故ret最后打印为7,这里就算不调用Add(3,4)程序也是有问题的,结果是不确定的,因为这取决于编译器在栈帧销毁的时候会不会清理,如果恰好编译器没有清理,那么结果可能是对的,但是过程是不正确的,要是编译器清理了的话,就会是随机值

image-20210927155703583

这里说明了如果返回的变量是一个局部变量时,引用返回是不安全的,因为当你调用完函数后,会销毁函数栈帧,这块空间已经不属于你了,但是你还是在通过引用能使用到,故这里发生了非法访问空间

那么这段代码有没有解决方法呢?c前面加static就可以了:

int& Add(int a, int b)
{
    static int c = a + b;
    return c;
}
int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

因为在加了static后,c就不是在Add函数的栈帧建立空间了,而是在静态区建立空间

总结:

一个函数要使用引用返回,该返回变量出了这个函数的作用域还存在,就可以使用引用返回,否则返回变量会销毁,就不安全


传值、传引用效率比较

函数传参传值和传引用的效率比较
#include <time.h>
struct A
{ 
    int a[10000]; 
};
void TestFunc1(A a){}
void TestFunc2(A& a){}
void TestRefAndValue()
{
    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();
    // 分别计算两个函数运行结束后的时间
    cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

image-20210929101913524

值和引用作为返回值类型的性能比较
#include <time.h>
struct A
{ 
    int a[10000]; 
};
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{
    // 以值作为函数的返回值类型
    size_t begin1 = clock();
    for (size_t i = 0; i < 100000; ++i)
    	TestFunc1();
    size_t end1 = clock();
    // 以引用作为函数的返回值类型
    size_t begin2 = clock();
    for (size_t i = 0; i < 100000; ++i)
    	TestFunc2();
    size_t end2 = clock();
    // 计算两个函数运算完成之后的时间
    cout << "TestFunc1 time:" << end1 - begin1 << endl;
    cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

image-20210929123209100


引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用一块空间,指针变量是开辟一块空间,存储变量的地址

在底层实现的角度,引用也是有空间的,因为引用是按指针的方式来实现的

指针的汇编代码:

image-20210929124506503

引用的汇编代码:

image-20210929124540648

引用的定义和引用访问跟指针定义和指针解引用在汇编层完全类似。

引用和指针的不同点:

  • 引用在定义时必须初始化,指针没有要求

  • 引用概念上定义一个变量的别名,指针存储一个变量地址

  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  • 没有NULL引用,但有NULL指针

  • 在sizeof中含义不同:引用结果为引用类型的大小(与类型有关),但指针始终是地址空间所占字节个数(32位平台下占4个字节)(与类型无关)

int main()
{
	double a = 10;
	double* pa = &a;
	*pa = 20;
	double& ra = a;
	ra = 20;
	printf("指针pa的大小:%d\n",sizeof(pa));
	printf("引用ra的大小:%d\n", sizeof(ra));

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image-20210929125134754

  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  • 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  • 引用比指针使用起来相对更安全


内联函数

假设swap这样的函数被频繁调用,能否有什么优化的方法?用法不变,不建立栈帧呢?

C语言的方法是什么呢?宏

宏这个东西虽然很有价值,但是他有不少缺点

1、语法复杂,主义的细节多,容易出错

2、没有类型安全的检查,在预处理阶段已经替换

3、不能调试

又有些东西来替代宏:推荐用const、enum、inline替代宏


概念

如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字inline,在调用函数之前需要对函数进行定义。

//调用的地方展开,没有函数栈帧的建立
//inline debug是不会展开的,是可以调试的
//release或者debug设置一下优化等级才会展开
inline int Add(int x,int y)
{
    return x+y;
}
int main()
{
    Add(2,3);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image-20210928092920661

我们可以看到汇编代码中,没有Add函数的call


特性

  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略掉内联。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

普通函数在链接阶段找函数定义,而内联是在编译阶段找函数定义,如果内联函数定义定义在头文件中,所有包含该头文件的编译单元都可以正确找到函数定义,然而,如果内联函数定义在编译单元A中,那么在其他编译单元中调用fun()的地方将无法解析该符号,因为编译单元A生成目标文件A.obj,内联函数fun()已经被替换掉,A.obj中不再有fun这个符号,链接器自然无法解析,所以,如果一个内联函数会在多个源文件当中用到,那么必须定义在头文件中


auto关键字(C++11)

auto的使用细则

int main()
{
    int a =1;
    char b='a';
    
    //通过右边赋值对象,自动推到变量类型
    //简化代码
    auto c = a;
    auto d = b;
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

类型太复杂,太长,auto自动推导变量类型,简化代码

缺点:一定程度上牺牲了代码的可读性

typeid可以去看变量的实际类型

cout<<typeid(c).name()<<endl;
cout<<typeid(d).name()<<endl;
  • 1
  • 2

image-20210927221000148

下面再看一个程序:

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;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

image-20210927220900019


auto不能推导的场景

  • auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
  • 1
  • 2
  • 3

只有赋值才能推导出类型

  • auto不能直接用来声明数组
void TestAuto()
{
    int a[] = {1,2,3};
    auto b[] = {456};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

C++98auto作用反正也没价值,所以C++只保留了auto作为类型指示符的用法


基于范围的for循环(C++11)

范围for的语法

C++11提供一种新的访问数组的方式

int main()
{
    int array[]={1,2,3,4,5};
    //自动依次取数组中的值赋值给e,自动判断结束
    for(auto e:array)
    {
        cout<<e<<" ";
    }
    cout<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image-20210927220229069

上面是打印,那么我们能改变数组吗?

答案是可以的,但是我们e需要是引用,因为需要修改数组需要修改数组本身

int main()
{
    int array[]={1,2,3,4,5};
    //自动依次取数组中的值赋值给e,自动判断结束
    for(auto& e:array)
    {
        e*=2;
    }
    for(auto e:array)
    {
        cout<<e<<" ";
    }
    cout<<endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

image-20210927220305366


范围for的使用条件

for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[])
{
    for(auto& e : array)
    cout<< e <<endl;
}
int main()
{
    int array[]={1,2,3,4,5};
    TestFor(array);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

范围for看起来很厉害,实际没什么东西,都是替换成普通访问了

指针空值—nullptr(C++11)

C++98中的指针空值

我们在定义一个指针变量时,都需要给它进行初始化,否则可能会造成野指针的问题,在C++98中初始化指针:

int* p1 = NULL;
  • 1

而C++11中初始化指针:

//C++11  推荐用它当空指针使用
int* p2 = nullptr;
  • 1
  • 2

指针本质是内存按字节为单位空间的编号,空指针并不是不存在的指针,而是内存第一个字节的编号,一般我们不使用这个字节存有效数据,用空指针一般用来初始化,表示指针指向没有存储一块有效数据的空间

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

void f(int)
{
	cout<<"f(int)"<<endl;
}
void f(int*)
{
	cout<<"f(int*)"<<endl;
}
int main()
{
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

image-20210927215408017

我们的本意是想通过f(NULL)来调用f(int*)函数,但是由于NULL被替换成了0,编译器将他看成了整形数字,所以进入了f(int)

C++11中,所以我们可以这样:

f(nullptr);
  • 1

image-20210927215731322

这时就调用了f(int*)函数

欢迎大家评论区学习交流!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/775005
推荐阅读
相关标签
  

闽ICP备14008679号