C++入门超详细解释

C++入门

框架

#include <iostream>
using namespace std;

int main()
{
    return 0;
}

命名空间 namespace (不常用)

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名
空间的成员。

命名空间的{}的后面不需要加分号,跟结构体不一样

//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
     // 命名空间中的内容,既可以定义变量,也可以定义函数
     int a;
     int Add(int left, int right)
     {
     	return left + right;
     }
}
//2. 命名空间可以嵌套
namespace N2
{
     int a;
     int b;
     int Add(int left, int right)
     {
     	return left + right;
     }

    namespace N3
    {
         int c;
         int d;
         int Sub(int left, int right)
         {
         	return left - right;
         }
     }
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{
    int Mul(int left, int right)
    {
     	return left * right;
 	}
}

这个命名空间,可以放变量进去,也可以放函数进去。就像结构体一样,但是又不太一样,因为不用在main主函数里面再次创建结构体了。这个命名空间拿起来就能用。

同一个工程里面相同名称的命名空间会自动合并。在不同的命名空间里面可以建立相同的变量和相同名称的函数(函数的功能完全不同的那种)

命名空间的使用方式(三种)

因为命名空间里面的成员不可以直接使用

namespace N
{
     int a = 10;
     int b = 20;
     int Add(int left, int right)
     {
     	return left + right;
     }
     int Sub(int left, int right)
     {
     	return left - right;
     }
}
  • 像这种直接使用命名空间里面的成员,就会报错,因为编译器无法识别
int main()
{
     printf("%d\n", a); // 该语句编译出错,无法识别a
     return 0;
}

正确方式:

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

这个作用域就是变量的来源,比如上面的例子的命名空间N

而作用域限定符就是 :: ,左边是作用域,右边是作用域里面的成员变量。 作用域名::成员名

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

using N::b;

如果经常使用这个变量b , 通过上面这种方式,这个b就是一个全局变量了。就不用再次声明b是来源于哪一个命名空间了。

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

能把命名空间的一个成员拿出来作为全局变量,所以,也能把这个命名空间全部拿出来当成全局变量。

怎么说呢,感觉,你在命名空间里面定义变量,再解开这个命名空间,还不如直接创建全局变量呢。

using namespce N;
int main()
{
     printf("%d\n", a);
     printf("%d\n", b);
     Add(10, 20);
     return 0; 
}

using namespace std;

关于using namespace std;学了命名空间这个概念之后,就会比较好解释了。

首先,std是c++的库

那么这句话就是把c++库里面的所有东西放到这个工程里面了

也就是命名空间的第三种使用方式。

这个方式也是相当于把std这个库全展开了,好处就是不用再声明变量是来自std库了,坏处就是再想命名的时候就会跟库里面的命名冲突

因为它是std这个命名空间里面的变量或者函数啊什么的。你要是还想再命名一个(同名的有冲突的)函数,也可以自己重新定义一个命名空间。因为命名空间里面的变量不会互相冲突

<iostream>

对于这个头文件 io stream

它是c++的输入流和输出流。就像c语言的<stdio.h>

不过,在c语言中printf,scanf这些函数是<stdio.h>这个库里面的函数。

在c++中,iostream这个库也有自己的函数来实现输入和输出的功能

对于一些,特别特别老的编译器(VC6.0),这个头文件也有这种写法<iostream.h>,因为实在是太老了。而且新版编译器都不支持这种写法,尽量忽略

cout

这个函数是c++中iostream库的输出函数,就跟printf一样。但是这个函数来自std这个命名空间。所以,想要使用这个函数,必须要带iostream头文件。关于std命名空间,有那三种方式如下

  1. 提前声明整个std命名空间
#include <iostream>
using namespace std; // 有这句话

int main()
{
    cout << "hello World";
    return 0;
}
  1. 给cout加上命名空间的作用域限制符"::"
#include <iostream>
//using namespace std;
int main()
{
    std::cout << "hello World";
    // 作用域限制符"::"
    return 0;
}

endl

这个函数跟cout的来源一样,iostream库的std命名空间的函数。

作用:换行

#include <iostream>
using namespace std; // 包整个std命名空间

int main()
{
    cout << "hello World"<<endl;
    return 0;
}


#include <iostream>
//using namespace std;

int main()
{
    std::cout << "hello World"<<std::endl; // 或者自己声明来源哪个命名空间
    return 0;
}

之前的换行方式也能继续用,如下:

#include <iostream>
using namespace std;

int main()
{
    cout << "hello World\n";
    return 0;
}

cin

相当于scanf,但是也不相同。也省去了自己写输入数据的类型这个步骤,以后就不用指定输入和输出的类型了。

#include <iostream>
using namespace std;

int main()
{
    int i = 1;
    double d = 1.11;
    cin >> i >> d; // 输入5 5.55
    cout << i << " " << d << endl;// 输出5 5.55
    return 0;
}

cout的使用

关于cout的使用,和printf有很大的不同。

cout输出的时候,不用区分变量的类型,这个函数可以自动识别变量是什么类型。只要定义过了。自己就会按照定义去输出。

#include <iostream>
using namespace std;

int main()
{
    int i = 1;
    double d = 1.11;
    cout << i << " " << d << endl; // 先输出i的值,再输出一个空格,再输出d的值,然后换行
    // 输出结果是:1 1.11
    return 0;
}

对比,下面这种写代码的方式。不难发现,每个std库里面的函数都声明命名空间就很麻烦。所以在日常练习中,不要在乎命名冲突。自己改名。

#include <iostream>
// using namespace std;

int main()
{
    int i = 1;
    double d = 1.11;
    std::cout << i << " " << d << std::endl;
    return 0;
}

命名冲突

由于c++的特性,你在命名的时候,如果不是特别必要,比如不是工程需求啥的,能自己换名称,尽量自己换。

如果真的有必要跟std库起冲突。也可以按照命名空间剩下的两种展开方式

  1. 只给常用的函数展开,比如cout、cin啥的,用using std::cout;这种方式展开局部对象。
  2. 不常用的函数就用命名空间作用域限制符来写std::cin;

到此为止算是c++的简单入门了,至少能看懂框架的每行代码是什么意思了


缺省参数(省钱的省)

缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

#include <iostream>
using namespace std;

void Func(int a = 0) // 设计函数参数的时候,可以给一个默认值,这个默认值在调用函数的时候生效。如果调用函数的时候,不给参数,这个默认值就会生效
{
    cout << a << endl;
}

int main()
{
    Func(); // 没有传参时,使用参数的默认值
    Func(10); // 传参时,使用指定的实参
    return 0;
}

就是设计函数的时候,函数的参数可以给个默认值,就像,int a=0 ,如果是放在以前,就只能是int a。现在可以直接给这个a赋值。当然,如果正常使用这个函数,也就是调用函数的时候该有的参数都有,一开始给a赋的值就无效了。如果少参数,这个a才会生效。就像现在的汽车备胎一样,其他的轮胎不坏,就一直不用备胎,一旦有坏的轮胎,备胎才会有用处。

这个Func();语句调用的时候,就没给参数,所以,a的默认值生效了

而Func(10);这个语句调用的时候给参数了,所以给的参数被函数正常使用了。a的默认值无效。

缺省参数分类

全缺省参数

全缺省的意思就是,所有的参数都有默认值。

void Func(int a = 10, int b = 20, int c = 30)
{
     cout << "a = " << a << endl;
     cout << "b = " << b << endl;
     cout << "c = " << c << endl;
}

半缺省参数

半缺省就是部分参数有默认值。不是真的一半参数有默认值

void Func(int a, int b = 10, int c = 20)
{
     cout << "a = " << a << endl;
     cout << "b = " << b << endl;
     cout << "c = " << c << endl;
}
  • 半缺省参数必须从右往左依次连续来给出,不能间隔着给

就是设计函数参数为缺省参数的时候,不能随便设计,不能说是函数的第一个参数和第三个参数是缺省参数,第二个参数是正常的参数

缺省参数设计的时候,只能是从右往左设计缺省参数

错误示范

void Func(int a = 0, int b , int c = 20) // 错误方式,其中第一个和第三个是缺省参数,不是从右到左依次连续的,会报错的
{
}
void Func(int a = 0, int b = 10, int c) // 也是错的,必须是从右往左连续
{
}

缺省参数函数的使用

  1. 对于全缺省参数犹如语句:Func(int a=0, int b=10, int c = 20)

传参的时候可以不给参数调用,也可以只给一个参数,或者只给两个参数,或者全部参数都给。

  • **在给部分参数的时候,参数只能从左往右依次赋值。**不能是(,1)或者(,1,)

逗号只是传参的时候,给数据的分隔,而不是告诉函数,你只想给哪个参数传参

#include <iostream>
using namespace std;

void Func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

int main()
{
    Func();  // 一个参数都不给 // 输出就是10,20,30
    Func(1); // 只给一个参数 // 输出就是 1,20,30
    Func(1, 2); // 给两个参数 // 输出就是1,2,30
    Func(1, 2, 3); // 给三个参数 // 输出就是1,2,3
    return 0;
}
  1. 对于半缺省参数函数

在传参的时候,至少也是必须,给非缺省参数的参数。就像下面这个半缺省参数函数。第一个参数不是缺省参数。那么在调用这个函数的时候,只要要有一个参数,而且这个一个参数从左往右开始赋值,也正好给到第一个参数a。

  • 如果不给第一个参数传参就会报错。

剩下的缺省参数可以选择性传参,当然,也只能是从左往右传参。

#include <iostream>
using namespace std;

void Func(int a, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

int main()
{
    Func();  // error 
    Func(1); // 只给一个参数 // 输出就是 1,20,30
    Func(1, 2); // 给两个参数 // 输出就是1,2,30
    Func(1, 2, 3); // 给三个参数 // 输出就是1,2,3
    return 0;
}

函数重载

函数重载:是函数的一种特殊情况,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); // 10是整型,编译器默认是个整型,就会按照int Add(int left, int right)这个语句去办事
     Add(10.0, 20.0); // 10.0是浮点型就会去调double Add(double left, double right)
     Add(10L, 20L); // 10L的10是long的类型。编译器会找参数是long的函数去调用

     return 0;
}

函数重载的要求

  • 参数类型不同
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;
}
  • 参数个数不同 (0个也算个数不同)
int Add()
{
    return 0;
}
int Add(int left)
{
	return left;
}
int Add(int left, int right)
{
	return left + right;
}
int Add(int left, int right, int mid)
{
	return left + right + mid;
}
  • 类型顺序不同
int Add(int i, char ch)
{
	return i+ch;
}
int Add(char ch, int i)
{
	return i+ch;
}

这三个有一个符合要求,就可以构成重载

对于函数前面的那个返回值,不能作为函数重载的依据

int Add(int left, int right)
{
	return left + right;
}
void Add(int left, int right) // error 不满足函数重载
{
	return;
}

int Add(int right, int left) // error 形参的名称对函数重载没有影响,函数重载看的是参数的类型,不是参数名称
{
	return left + right;
}

也就是只有返回值不同,不管用

函数重载的使用

#include <iostream>
using namespace std;

int Add(int i, char ch)
{
	cout << i << " " << ch << endl;
	return i + ch;
}
int Add(char ch, int i)
{
	cout << ch << " " << i << endl;
	return i + ch;
}
void Add()
{

}
int main()
{
	Add(); // 没有参数,程序先去找有没有这个函数,发现有这个函数void Add(),虽然是空,但是不影响程序
	Add('a',5); // 第一个参数是一个字符,第二个参数是整型。于是程序找到了第二个函数int Add(char ch, int i)
	Add(10,'b'); // 整型+字符,即第一个函数int Add(int i, char ch)
    
    return 0;
}
/*
输出:
a 5
10 b

*/

函数重载的面试题

  1. 什么是函数重载?

答:在c++环境里面,函数名相同,但是函数参数不同。就叫函数重载。函数参数的不同可以有三种情况;类型不同 或者 个数不同 或者 顺序不同 ,满足这三种其中一个条件就可以构成函数重载。对函数的返回值没有要求

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

答:这个问题和程序的编译链接有关

下面这部分是对程序预处理的复习

对于一个工程来说,至少有三个子文件:list.h list.c test.c

  1. 预处理 头文件展开,宏替换,条件编译,去掉注释 生成list.i test.i

  2. 编译 检查语法,生成汇编代码 生成 list.s test.s

  3. 汇编 把汇编代码转成二进制机器码 list.o test.o

  4. 链接 把两个目标文件链接在一起

    • 关于链接
    • 在链接的前一阶段中的汇编阶段,遗留了许多问题。比如在tset文件中用到一个函数,但是这个函数在另一个.c文件里面实现,所以在汇编阶段会在tset文件标记这个函数的地址在其他文件中。汇编阶段,会有符号表这个概念,符号表里面就是是这个函数的名称和该函数的地址。
    • 这个问题在链接阶段实现,也就是把两个文件链接了。

名字修饰(name Mangling)

在编译阶段中,由于编译器对函数名称处理的方式不同,才有了c语言和c++的不同。

在c语言的编译器编译阶段,函数名称会直接保留到汇编语言中,不发生改变

而在c++的编译器的编译下,函数名称会被重新修饰

//对同一个函数来说
//比如
int Add(int a,int b)
{
}
void func(int a,double b,int* c)
{
}

// 这两个函数在c语言编译器编译下,函数名还是(Add)和(func)

// 而在c++编译器编译的情况下,函数名发生改变
// 比如(Add)函数名,被改成了(_Z3Addii)
// (_Z3Addii)是什么意思呢?
_Z 	是统一的函数名前缀
3 	是函数名字符的个数
Add	是原函数名
ii	是函数参数的类型简称,第一个i是函数第一个参数的类型,第二个i是函数第二个参数的类型
// 同理(func)函数名被修饰成(_Z4funcidPi)
_Z	前缀
4	函数名字符数
func函数本来的名字
i	是第一个参数的类型
d	是第二个参数的类型
Pi 	是第三个参数的类型

请添加图片描述

请添加图片描述

通过这里就理解了C语言没办法支持函数重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。 另外我们也知道了,为什么函数重载要求参数不同!而跟返回值没关系。。。
  • 所以函数的参数不同,函数在编译阶段生成的名称就不同

  • 因为函数名称不同,所以不同函数有不同的地址

    • 再去调用函数的时候,就知道调哪个函数了
  • 还是因为c++会对函数名进行修饰,这些修饰也就是区分不同函数的关键

    以上演示是在Linux环境下演示,而windows环境下的命名规则更复杂,但是道理是相同的。就不再重复演示了。

extern “C”是干啥的(面试题)

extern "C" int Add(int left, int right);

int Add(int left,int right)
{
    return left+right;
}
int main()
{
 	Add(1,2);
 	return 0;
}

官方解释

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。

我的理解

在写工程文件的时候,本来要求你用c++写代码。突然,想让你把其中一个函数让c语言也能识别。然而,你这个工程本来就是c++环境。通过前面的知识,你也知道这两种语言的编译器编译函数名称的不同,想要直接让c语言去用这个函数,是不可能的。但是由于c++编译器兼容c语言。所以,想到了一个方法,也就是在这个函数前面加一个语句entern “C”,告诉编译器,这个函数要按照c语言去编译。

既然按照c语言去编译了,那么c语言编译器可以直接食用了。但c++编译器就不一定能食用了。所以设计这个语句的时候也考虑了这个问题。这个语句还完成了一个工作,就是告诉c++编译器,这个函数是c语言版的函数,要按照c语言去食用。所以,这个函数被暂时用c语言编译了。

一个函数对应一个extern “C”,想转换几个函数就写几个。一般只会有少量函数给外部程序使用。

总结:

extern “C"语句完成了两个任务

  1. 告诉c++编译器把这个函数按照c语言去编译 ,因为是按照c语言去编译,所以命名规则也变了,也就不能函数重载啥的了
  2. 告诉c++编译器,这个函数要按照c语言去食用

缺省参数会影响函数重载吗?(面试题)

下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?

void Func(int a = 10)
{
 	cout << "void TestFunc(int)" << endl;
}
void Func(int a)
{
 	cout << "void TestFunc(int)" << endl;
}
// 根据c++的命名规则,都是看参数类型,这两个函数的函数名相同,参数的类型也相同都是i , 所以无法区分

引用(别名)

引用就是给一个变量取新的名字,而且原来的名字还能接着用。

物理意义上,也就是对同一个空间取多个名字

引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

格式: 类型& 引用变量名(对象名) = 引用实体;

int main()
{
    int a=1;
    
    int& ra=a; // 引用
    int& rb=a;
    int& rc=rb;
    
    printf("%d %d %d %d\n",a,ra,rb,rc); // 1 1 1 1
    printf("%p %p %p %p\n",&a,&ra,&rb,&rc); // 都是同一个地址/空间
    
    rc=2; // 改变rc指向空间的值
    printf("%d %d %d %d\n",a,ra,rb,rc); // 2 2 2 2
    printf("%p %p %p %p\n",&a,&ra,&rb,&rc); // 都是同一个地址/空间
    
	return 0;
}

注意:引用类型必须和引用实体是同种类型的

引用的特性

  1. 引用在定义时必须初始化
int main()
{
    int a=1;
    int& b; // error 必须初始化
    return 0;
}
  1. 一个变量可以有多个引用
int main()
{
    int a=1;
    
    int& ra=a; // 多个引用
    int& rb=a; // 多个引用
    int& rc=rb;// 多个引用 
    
	return 0;
}
  1. 引用一旦引用一个实体,再不能引用其他实体
int main()
{
    int a=1;
    int& b=a; // 创建a的引用,b // b的类型是int,不是int&
    
    int c=2;
    b = c; 	// 分析此处:是b是a的引用还是c的引用?还是把c的值赋给a或b?
    
    // 经过取地址调试,可以发现,b始终是a的引用。最后a变成了2.
	return 0;
}
  1. 引用取别名的时候,变量的权限可以缩小,但是不能放大
int main()
{
    // 正常
    int a=1;
    int& b=a;
    
    // 不允许变量权限放大
    const int a=1;
    int& b=a; // error // a在定义的时候有const修饰,是只读的权限,但是b这个别名在定义的时候没有const修饰,那么b是可读可写的。是权限放大。不被允许
    
    //允许变量权限缩小
    int a=1;
    int& b=a;
    const int& c=a; // 在这里a是可读可写,但c是只读。属于权限缩小。被允许。
    	 // 也就是在使用的时候,c这个变量可以被使用,但是不能被修改。 对于a、b变量可以修改和读取。
    return 0;
}
  1. 引用时可以取不同的类型,但是只允许从变量到别名的权限缩小
int main()
{
    int a=1; // 一个正常的变量
    
    double b=a; // 这里是用 a 的值给 b 赋值 // 这里的 a的值 强制类型转换给了 b // 在类型不同的时候,编译器会进行自动的类型转换,但是又不能改变原来的值a,所以会产生一个临时的double类型的变量,然后再把这个临时的变量的值赋给 b
    
    double& c=a; // error // 在取别名的时候,如果类型不同,会产生一个临时变量,但是这个临时变量不能逆向改变,比如,这里的a的值只能是从int转换为别名的double,可是c这个别名是double的类型,是可读可写的。又因为不能在类型不同的时候通过临时变量逆向改变最开始的变量a。所以编译不通过。
    
    const double& d=a; // yes // 这里对d进行了const限制,虽然是double类型,但是这个类型不会通过临时变量逆向改变a,所以允许转换。// 但是这个别名d的值也就被锁定了
    
    const float& e=a;  // yes // e 和 d 同理
    
    return 0;
}

上面的a与b没有直接关系,b只是得到了a的值。b的变化跟a没关系。赋值不涉及权限等问题。

但是c,d,e都是a的别名,他们的改变会影响a。可是类型又不同,还不能逆向改变变量a的值。所以必须用const修饰

在赋值的时候产生的临时文件,是一个常量,也不能说是常量,应该是具有常性。也就是说有常量的性质。

  • 关于权限的缩小和放大的规则适用于引用和指针

从现在开始,变量被称为对象。

常引用

就是在引用的时候,加上const进行修饰。加上const 进行修饰之后,引用的类型可以不同。给别名加上const进行限制,相当于创造了一个常量。所以可以换类型。

这个const属于缩小权限,也就是程序对变量修改的权限。当没有const的时候,别名也可以之间修改原变量。加上const限制之后,别名就定死了,不能再改变了。

引用的使用

  1. 引用 作参数
// 取别名做参数
void Swap_cpp(int& r1, int& r2) // 这里的引用,只有在传参的时候,才被定义 // r1是a的别名,r2是b的别名
{
     int tmp = r1;
     r1 = r2;
     r2 = tmp;
}
// 用指针做参数
void Swap_c(int* r1, int* r2) // r1是a的指针,r2是b的指针
{
     int tmp = *r1;
     *r1 = *r2;
     *r2 = tmp;
}
int main()
{
    int a=5,b=10;
    Swap_c(&a,&b);
    Swap_cpp(a,b);
	return 0;
}
  1. 引用做返回值

在学这块知识的时候,要先了解一个概念。就是函数在传值的时候,一般会产生一个临时变量。这个临时变量是目标变量的临时拷贝,然后才是把临时变量的值交给结果变量。比如,Add(int a,int b),再调用这个函数的时候,不是把值直接给ab变量的,而且ab的临时空间得到了值,再传给ab。

是不是有一点点浪费空间,有一点点臃肿。明明我已经有了一个变量自己的空间,现在还要再申请一个空间再存值取值

取别名这个操作完美解决了这个问题,在取别名这个操作下,想要传值的时候,是直接把这个空间(类似地址的东西)递过去了。

就不需要创建临时变量了。下面的例子也是为了解释这个问题。

void swap(int r1,int r2)  // 这是一个传值的函数,那些值在传过来的时候会先创建临时两个临时副本,然后才把临时空间的值给r1r2两个变量
{
    int tmp=r1;
    r1=r2;
    r2=tmp;
}
void swap(int& r1,int& r2) // 这是一个传引用做参数的函数,那些作为参数的值传过来的时候不用创建临时空间,r1和r2会直接根据传过来的值的空间进行引用
{
    int tmp=r1;
    r1=r2;
    r2=tmp;
}

int Count1()  // 这是一个返回值是传值的函数,在这个函数调用结束后会返回一个值,这个值也就是n的值,n是有一个自己的空间的。
{     	//在传返回值的时候,这个空间被浪费了,用的是n的临时拷贝空间传出去的。当然传出去的值是放在临时空间的。这个空间具有常量性

    static int n=0;
    n++;
    return n;
}
int& Count2()  // 这是一个返回值是传引用的函数,返回值是n的值,n本身有一个空间,因为是传引用出去。所以,直接利用n自己的空间,传值出去了
{
    static int n=0; // static不会影响变量的类型,只是会影响变量的生命周期
    n++;
    return n;
}
int main()   
{
    int& r1=Conut1(); // error // 因为Conut1是传值返回值,得到的空间是一个临时空间,具有常量性,而r1是int类型,不是常量
    int& r2=Conut2(); // COnut2的返回值是n的空间本身,类型是int,而且r2的类型也是int。所以,可以直接接收
    
    const int& r1=Conut1(); // 只有r1被const限制之后具有常量性,才能接收Conut1的返回值
    return 0;
}

总结:

  • 凡是临时变量都具有常量性
  • 传值返回会多一个临时空间,这个临时空间是对值的拷贝
  • 而引用返回,直接传回来的空间就是那个值自己的空间

引用的不安全

int& Add(int a,int b)
{
    int c=a+b;
    return c;
}
int main()
{
    int& ret=Add(1,2);
    Add(3,4);
    cout << "ret=" << ret << endl; // ret是一个随机值,因为这个ret的空间已经被覆盖了,也可能是原来的值,要看编译器。所以不安全
    return 0;
}

在上面这个示例中可以看到,如果Add的返回值c的生命周期只在Add函数内部,但ret是c的别名,这个时候再调用ret,ret虽然是保存c的空间,但是已经是非法访问了。所以引用也是不安全的。

  • 很明显,就是c的生命周期太短了。虽然Add算出了结果,这个结果保存在了c的空间里面,ret也是这个空间的别名。但是出了Add函数空间就失效了。
  • 所以为了解决这个问题,可以用static,c的生命周期就变成了整个程序有效了。
// 改良版
int& Add(int a,int b)
{
    static int c=a+b; // 这个c只有在函数第一次被调用的时候才定义,也就是说只有Add(1,2)中创建了c,且c的值是3。c已经是一个全局变量了
    return c;
}
int main()
{
    int& ret=Add(1,2);
    Add(3,4); // 第二次调用函数的时候,c已经被创建好了,所以对于第二次函数调用而言,没有执行任何操作,只是把之前c的值传出来了
    cout << "ret=" << ret << endl; // ret = 3
    return 0;
}

经过static修饰后,c是建立在数据段的常变量。数据段的空间不会被销毁。ret一直是Add(1,2)里面c空间的别名

static修饰过的语句只会执行一次!所以第一次执行过后,c的值就固定了。

int& Add(int a,int b)
{
    static int c=a+b; 
    c=a+b;  // 除非再给c重新赋值,并且没有static修饰
    return c;
}
int& ret=Add(1,2); // ret=3
    Add(3,4);   // ret=7
总结:

用引用返回值的时候,要看看这个返回值的生命周期,如果只是在他自己的函数内部有效,就不安全了,因为这块空间可能被使用,或者被覆盖。

如果返回值是一个全局变量,可以考虑用引用返回。

引用的好处

  • 少创建一个临时变量——提高效率
  • 作输出型参数——提高效率
  • 其他作用以后讲

传值返回和传引用返回的差别

#include <iostream>
#include <time.h>
using namespace std;
struct A 
{ 
	int a[10000];
};
A a;
// 值返回
A Func1() 
{ 
	return a; 
}
// 引用返回
A& Func2() 
{ 
	return a;
}
void TestReturnByRefOrValue() // 测试函数返回值是 引用 或 值 的差异
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		Func1();
	size_t end1 = clock();

	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		Func2();
	size_t end2 = clock();

	// 计算两个函数运算完成之后的时间
	cout << "Func1 time:" << end1 - begin1 << endl; // 165 ms
	cout << "Func2 time:" << end2 - begin2 << endl; // 1 ms
}

int main()
{
	TestReturnByRefOrValue();
	return 0;
}

上面的测试中,func1是传值返回,他的返回值是结构体a的临时拷贝

func2的函数返回值是a的引用,也就是a自己本来的空间,没有额外创建新的空间

传值做参数和传引用做参数的差异

#include <iostream>
#include <time.h>
using namespace std;
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 < 100000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; // 83 
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; // 0
}
int main()
{
	TestRefAndValue();
	return 0;
}
  • 经过上面这两个测试用例发现,无论是传值做参数,还是传值返回,都会浪费一定的效率
  • 传引用做参数或返回值都能解决这个问题,也证明了引用可以提高效率

引用作返回值的适用场景

  • 返回值是一个全局变量
  • 静态变量

引用和指针的区别

请添加图片描述

在语法上来说,

在创建时,引用,直接就是在原有的空间上取别名。

指针则是新建一个空间,存放原空间的地址。有新的空间产生

但是对于底层来说,不一样。

eax,[a]的意思,是把a的地址给eax。所以,后面就是eax再把地址给b和p.指针和引用在汇编语法上是一样的。

但是在底层逻辑上一样。

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
    实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
    4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

内联函数inline

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。

内联函数解决的问题

因为某些函数在程序执行的过程中被频繁调用。哪怕再小的函数,也需要建立函数栈帧,也会有消耗的。

就是从主函数到调用的函数的过程中,会进行压栈的操作。也就是程序会不断的从主函数的栈帧跳到调用的函数的栈帧

  • c语言中可以使用宏函数
  • c++使用内联函数

他们都是在预编译阶段,使用宏替换,在函数中展开的。c语言的做法是有缺点的:宏不能调试、可读性很差

对于c++来说,有了内联函数的概念了,只需要在相应的函数前面,再加上inline语句。这个函数在程序的预编译阶段就会被展开到函数中去。(观察代码的汇编语言中是否有call语句。call语句是使用外函数的意思。当然内联函数就没有call了)

#include <iostream>
#include <time.h>
using namespace std;
inline void Swap(int& r1, int& r2) // 改成内联函数
{
    int tmp = r1;
    r1 = r2;
    r2 = tmp;
}
//void Swap(int& r1, int& r2) // 原函数
//{
//    int tmp = r1;
//    r1 = r2;
//    r2 = tmp;
//}
int main()
{
    int a = 1, b = 2;
    Swap(a, b);
    cout << a << b<<endl;
    return 0;
}

很明显,想要把常用函数改成内联函数只需要在函数前面再加个inline就可以了。

这个常用函数还是保持正常的函数结构。————函数的可读性强

在写函数的时候节省大量的时候,不用多次写重复的语句了。

内联函数的特性

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

    用inline展开之后。函数所占用的空间,就是(主函数自己的+内联函数*次数)之和。所以inline的函数要么不能频繁使用,要么自己本身的语句要少

  • 类似递归或者超过20行代码的函数,就不会内联了(编译器自动优化)。

  • 因为内联函数是会展开到主函数中,那么在展开的时候,就不构成函数了,也就没有内联函数的地址。如果此时,函数的声明说,这个函数是内联。程序是找不到函数的。

// F.h
#include <iostream>
using namespace std;
inline void f(int i); // 函数的声明是内联,但是函数的原型不在这个.h文件中

// F.cpp
#include "F.h"
void f(int i)
{
 	cout << i << endl;
}

// main.cpp
#include "F.h"
int main()
{
     f(10); // error
     return 0;
}

宏的优缺点?(面试题)

优点

  1. 增强代码的复用性。
  2. 提高性能。

缺点

  1. 不方便调试宏。(因为预编译阶段进行了替换)
  2. 导致代码可读性差,可维护性差,容易误用。(太难设计)
  3. 没有类型安全的检查 。

C++有哪些技术替代宏?

  1. 常量定义 换用const
#define N 10			// c语言
const int N = 10;		// c++优化版
  1. 函数定义 换用内联函数

宏函数————》》》inline函数替换

以上所有的知识都是C++98支持的

auto关键字(C++11)

官方定义:

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

就是说auto这个类型关键字,可以自己推出要定义的变量应该是什么类型。

#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    auto b = a;
    int& c = a;
    auto& d = a;
    auto* e = &a; 
    auto f = &a;

    cout << typeid(b).name() << endl; // typeid(b).name()这个语句可以打印括号里面参数的类型
    cout << typeid(c).name() << endl;
    cout << typeid(d).name() << endl;
    cout << typeid(e).name() << endl;
    cout << typeid(f).name() << endl;

    return 0;
}
/*
输出:
int
int
int
int * __ptr64  // e是指针类型,__ptr64意味着是本机是64位系统
int * __ptr64	// e和f的类型相同.所以,用auto的时候,就不用再自己加”\*“了.
*/

auto的使用规则

  1. 使用auto定义变量时必须对其进行初始化
auto e; // 无法通过编译
  1. auto与指针和引用结合起来使用

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

int a = 0;
int& b = a;
auto& c = a; // c是对a的引用,c的类型是auto,auto推导出c的类型是int。auto只是一个类型。类型& 变量=变量。才是取别名
  1. 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

void TestAuto()
{
 	auto a = 1, b = 2; // 允许同一行是一个类型
 	auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 // 同一行类型不同
}
  1. auto不能作为函数的参数

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

    因为在编译的时候,这个函数虽然没有被使用,哪怕这个程序也不使用。但是在编译阶段依旧要对这个函数建立栈帧。如果是auto作参数,参数类型未知那么auto所需要的空间(字节)也未知

  2. auto不能直接用来声明数组

void TestAuto()
{
 	int a[] = {1,2,3};
 	auto b[] = {456};
}

auto最大的作用

在以后使用容器的时候,可以优化变量的类型。

#include <iostream>
#include <map> // 未来学习的容器map
using namespace std;

int main()
{
    std::map<std::string, std::string> dict;
    std::map<std::string, std::string>::iterator it1 = dict.begin(); // 这就是创建了一个变量it1,类型名称很长
    auto it2 = dict.begin(); // 使用auto,可以简化那一大段类型名称
    return 0;
}

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

#include <iostream>
#include <map>
using namespace std;

int main()
{
	int array[] = { 1, 2, 3, 4, 5 };
	// 把array数组乘2倍,再打印出来
	// C语言的做法
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
	{
		array[i] *= 2;
	}
	for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); p++)
	{
		cout << *p << endl;
	}

	// C++11 范围for的用法
	for (auto& e : array) // auto自动类型名,auto&是取别名的意思。auto& e就是
	{
		e *= 2;
	}
	for (auto e : array)
	{
		cout << e << " ";
	}
	return 0;
}

上面这个示例中,展示了两种解决问题的方法

一种是c语言的做法,比较简单不再解释

第二种,就是c++11 中“范围for”的使用

for (auto& e : array) // 注意是auto& e,是引用

auto自动类型名,auto&是取别名的意思。auto& e就是一个准备取别名的变量e。:array的意思就是从array这个数组中,从数组的0到结束依次取数值出来。对!就是自动取值,自动结束!

连上前面的auto& e。整体for (auto& e : array),意思就是从array这个数组中取值出来,给这个值取别名 e 。然后再对e进行操作

e *= 2; // e变成原来的二倍,取别名的原空间的值也就变成了二倍。

for (auto e : array) // 注意是auto e,是赋值

意思就是:从这个数组名为array的数组里面,从偏移量为0的地方取值出来,赋值给e。等e完成一次循环之后,再向后移动一个步长,取数组的下一个值出来,交给e。

反正这个“范围for”用起来很方便。

这是c++池里面的一个语法操作,因为用起来实在是方便,就像吃糖一样,也叫语法糖

目前知道可以这么用,就够了。

范围for的使用条件

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

    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

    注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[]) // 这里的array是一个指针,不是真正的数组名 
{
 	for(auto& e : array) // 会报错
 	cout<< e <<endl;
}
  1. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)

指针空值nullptr(C++11)

先上个例子

#include <iostream>
using namespace std;
void fun(int a)
{
	cout << "整型" << endl;
}
void fun(int* a)
{
	cout << "整型指针" << endl;
}
int main()
{
	fun(0);
	fun(NULL);
	fun(nullptr);
	return 0;
}
/*
输出:
整型
整型
整型指针
*/

很明显,第二个出错了。明明NULL就是指针置空的。但是错了。

原因就是C语言中,NULL是被宏定义出来的。也就是#define NULL 0

NULL就是0,0的类型是int

一个 int 类型的 0,给一个 int* 类型的指针置空???

所以在C++11中,新增一个常量nullptr。

这个新增的常量nullptr的类型是void*

满足了日常写代码对指针的正确初始化的操作。如果还是用NULL,可能会发生意外情况。

nullptr也是宏替换。

  • NULL是int类型的0

  • nullptr是void*类型的0

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值