【C++】C++入门特性全解析

大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞关注一下白晨吧!你的支持就是我最大的动力!

【C++】C++入门特性全解析_第1张图片

文章目录

  • 前言
  • C++入门特性
    • 1.命名空间
      • 1.1 命名空间的定义
      • 1.2 命名空间的用法
      • 1.3 命名空间的特性
    • 2.C++的输入与输出
    • 3.缺省参数
      • 3.1 缺省参数的定义
      • 3.2 缺省参数的分类
    • 4.函数重载
      • 4.1 函数重载使用详解
      • 4.2 重载和const形参
      • 4.3 重载函数的调用过程
      • 4.4 extern “C”
    • 5.引用
      • 5.1 引用的定义
      • 5.2 引用的特性
      • 5.3 引用与const
      • 5.4 引用的函数传参
      • 5.5 引用作返回值
      • 5.6 引用与指针的区别
    • 6.内联函数
      • 6.1 内联函数的定义
      • 6.2 内联函数的特性
    • 7.C++11常用的特性
      • 7.1 auto关键字
      • 7.2 范围for语句
      • 7.3 nullptt常量
  • 后记

前言


C语言作为一门面向过程的语言,虽然更接近于底层,但是由于本身的历史过于悠久,在一些情况下会显得十分笨拙。C语言就好比中国厨师的菜刀,只有一把,但是只要自身的技艺足够精湛,就可以用一把刀切菜,雕花,达到一种返璞归真的境界。

而C++作为面向对象的语言,就更像是西式餐厅里面的刀,数量繁多,种类各异,针对不同的食材可以使用不同的刀来更好的处理食材。

类比上面的举例,C语言的语法当然是更加好学的,但是相对的专精C语言也是很难的,并且由于它的各种局限性,在一些情况下的方便程度不如C++。当然,C++语法也是出了名的难,就和西式厨房的刀一样多和杂,至少要比同样流行Java难很多。C++起源于C语言,并在C语言上增添了许多特性,比如:封装、多态和继承,所以C++还需要C语言的功底,这就导致了C++的上手难度很高,没有C语言功底的同学建议先去学习C语言(C语言专栏)。我会尽量将这些概念解释清楚,但是本人能力有限,如果有解释错误或者不清的地方,还请各位在评论区中指正。

C++入门特性


1.命名空间


1.1 命名空间的定义


命名空间是用来组织和重用代码的。

如同名字一样的意思,NameSpace(名字空间),之所以出来这样一个东西,是因为人类可用的单词数太少,并且不同的人写的程序不可能所有的变量都没有重名现象,对于库来说,这个问题尤其严重,如果两个人写的库文件中出现同名的变量或函数(不可避免),使用起来就有问题了。

为了解决这个问题,引入了名字空间这个概念,通过使用 namespace xxx;你所使用的库函数或变量就是在该名字空间中定义的,这样一来就不会引起不必要的冲突了。

再说通俗一点就是,在一个C++工程中,往往会有许多程序员进行开发,每个人都会定义大量的变量和函数,这样会导致重定义使程序无法正常编译链接,为了避免这种情况,就出现了命名空间这个概念,用来限定作用域。

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

namespace spaceName
{
	// 变量,函数,类等
}

命名空间中可以放变量,函数,类等数据

eg.

namespace baichen
{
	int i = 0;

	int add(int x, int y)
	{
		return x + y;
	}
}

namespace zhangsan
{
	int i = 0;
	FILE* pf = fopen("test.txt", "w");
}

上面的例子就很好的说明了这一点,由于两人都定义了i 变量,如果不加以限制,就会出现重复定义的情况。但是如果我们使用了命名空间就可以很好的防止重复定义的情况发生。


1.2 命名空间的用法


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

namespace spaceName
{
	// 变量,函数,类等
}

命名空间中可以放变量,函数,类等数据

那么我们应该如何访问命名空间中的成员呢?

::是C++ 中的作用域运算符,也叫域解析操作符、作用域限制符,它的作用是指明操作对象所处的空间

了解了作用域运算符后,这里我们介绍三种使用命名空间的方法:

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

    baichen::i = 3;
    fprintf(zhangsan::pf, "%d" , 3);
    
  2. 使用using将命名空间中成员引入

    using baichen::add;
    
    int main()
    {
    	cout << add(1, 2) << endl;
    }
    

【C++】C++入门特性全解析_第2张图片

  1. 使用using namespace 命名空间名称引入

    using namespace baichen;
    using namespace zhangsan;
    

    这样做会带来一些副作用——将命名空间中所有的成员都放了出来,但是你只是想用其中一两个成员,这样你就得考虑是否会与放出的命名空间中的成员发生重定义。所以,这种方式在工程中不建议使用。

这里我要提醒大家,在一个空间下使用其他空间的函数、变量等,都需要用以上三种方式来指明空间,否则会发生编译或者链接错误

换句话说,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中 ,其它空间使用本空间的数据必须指明空间。

【C++】C++入门特性全解析_第3张图片


1.3 命名空间的特性


  1. 命名空间可以嵌套

    namespace baichen
    {
    	int i = 0;
    
    	namespace s1
    	{
    		int i = 0;
    		int j = 0;
    	}
    }
    

    这里我们发现,baichen 空间下有两个 i ,这样的定义是合法的,因为两个 i 的命名空间本质上还是不同,我们可以从下方的使用中见得。

    int main()
    {
    	baichen::i = 3;
    	baichen::s1::i = 4;
    
    	cout << baichen::i << endl;
    	cout << baichen::s1::i << endl;
    	return 0;
    }
    
    

    使用嵌套的命名空间的变量时,需要用作用域运算符完整的指明空间 。两个 i 本质上空间不同,所以就不会出现重定义的情况。

【C++】C++入门特性全解析_第4张图片

  1. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

    namespace baichen
    {
    
    	int add(int x, int y)
    	{
    		return x + y;
    	}
    }
    
    namespace baichen
    {
    	int i = 0;
    
    	namespace s1
    	{
    		int i = 0;
    		int j = 0;
    	}
    }
    

    这也就要求我们,合并的空间中不能出现重定义,否则还是会出现问题。

【C++】C++入门特性全解析_第5张图片


2.C++的输入与输出


我们知道C语言中的输入输出我们一般用scanfprintf,而由于C++是支持C语言的,所以scanfprintf是可以在C++中使用的。但是C++中也有自己的输入与输出。

简例:

#include 

using namespace std;

int main()
{
	char s[20] = { 0 };
	cin >> s;
	cout << s << endl;
	return 0;
}

【C++】C++入门特性全解析_第6张图片

iostream 是 Input Output Stream 的缩写,意思是“输入输出流”。

由上可知,C++的输入输出必须包含头文件 ,同时要展开一个叫 std 的命名空间。这里是为了声明使用 cincout,如果不将 std 展开,会报错。

当然上文就已经提到了直接将一个命名空间展开可能会带来一些问题,所以为了安全,我们可以使用展开命名空间中成员的方法:

using std::cin;
using std::cout;
using std::endl;

这样就可以避免重定义的情况发生。

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持 格式,后续编译器已不支持,因此推荐使用 + 展开命名空间中的成员的方式

这种形式是不用展开命名空间的,但是相应的会有问题,并且这种头文件很久都没有更新,可能会有支持问题,不推荐使用。

在C++中,标准库定义了4个IO对象,分别是:

  • cin —— 标准输入
  • cout —— 标准输出
  • cerr —— 标准错误
  • clog —— 输出程序运行中的一般信息

这里我们要强调一件事情:cincout 不是C++中的关键字,而是类。(这里没有接触过类的同学,只用知道前一句就可以)

同时,使用 cout 进行输出时需要使用 << 运算符,使用 cin 进行输入时需要使用 >> 运算符,这两个运算符可以自行分析所处理的数据类型,因此无需像使用 scanfprintf 那样给出格式控制字符串。

  • >> 是输入运算符,又名流提取运算符。
  • << 是输出运算符,又名流插入运算符。

endl 表示换行,相当于C语言中的 \n

所以,C++中的输入输出会比C语言中的强格式化输入输出简单,但是如果要输入输出强格式的数据,还是推荐使用C语言的版本,因为C++要控制格式非常麻烦。


3.缺省参数


3.1 缺省参数的定义


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

eg:

void Print(int i = 10)
{
	cout << i << endl;
}

上面的这个函数,如果不给其传值,则打印 10,如果传值,则打印传的值。

【C++】C++入门特性全解析_第7张图片

缺省参数除了可以用常量指定,也可以用表达式指定。

eg.

float d = 2.0;

void f(float i = d + 2.3)
{
	cout << i << endl;
}

【C++】C++入门特性全解析_第8张图片

这里要注意,当指定 i 时,无论后面表达式是什么,i 的值就是给定的值。


3.2 缺省参数的分类


  • 全缺省参数

    函数参数全部有指定的默认值。

    void TestFunc(int a = 1, int b = 2, int c = 3)
    {
    	cout<<"a = "<<a<<endl;
    	cout<<"b = "<<b<<endl;
    	cout<<"c = "<<c<<endl;
    }
    

【C++】C++入门特性全解析_第9张图片

  • 我们调用全缺省的函数时,可以选择不传参,部分传参和全传参这三种调用方式。

  • 传参的顺序是从左到右,不能跨顺序传参

  • 半缺省参数

    函数参数部分有缺省值。

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

    上述参数 a 没有默认值,所以调用时, 至少要传一个参数来指定 a

【C++】C++入门特性全解析_第10张图片

  • C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值
  • 所以半缺省参数必须从右往左依次来给出,不能间隔着给。

以下为错误示范:

void Func(int a = 1, int b, int c = 3)
void Func(int a = 1, int b = 2, int c)
void Func(int a = 1, int b, int c)

注意:

  1. 默认值必须为常量、全局变量或前两者的表达式。

  2. 函数调用时实参按其位置解析,默认实参负责填补函数缺少的尾部实参(靠右位置)。

  3. 函数声明中也可以添加默认值,但是这个默认值只能是定义中默认值的补充,声明和定义中不能同时出现同一个参数不同默认值,否则编译器将无法区分。我们一般习惯上将其放在声明中,方便查看。

    //test.h
    void F(int a = 1);
    // test.c
    void F(int a = 2)
    {}
    // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
    
    //test.h
    void Func(int a = 1int b = 2, int c = 3);
    // test.c
    void Func(int a, int b, int c = 3)
    {}
    // 上面的是正确的,声明添加了默认实参
    


4.函数重载


4.1 函数重载使用详解


如果同一个作用域内的几个函数名字相同,但是形参列表却不相同,我们称之为函数重载

例如:通过一个 Swap 函数交换不同类型的变量

// 交换 int 变量的值
void Swap(int* a, int* b) 
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
//交换 float 变量的值
void Swap(float* a, float* b) 
{
    float temp = *a;
    *a = *b;
    *b = temp;
}
//交换 char 变量的值
void Swap(char* a, char* b) 
{
    char temp = *a;
    *a = *b;
    *b = temp;
}

编译器会自动根据传递参数的类型决定到底要执行哪一个函数,所以函数重载的参数一定要不同。

参数不同又可以分为以下三类:

  1. 参数类型不同

    void Swap(float* a, float* b);
    void Swap(char* a, char* b);
    
  2. 参数个数不同

    int add(int a, int b);
    int add(int a, int b, int c);
    
  3. 参数顺序不同(本质上也是参数类型不同)

    int GetTime(int day,double time);
    int GetTime(double time,int day);
    

注意:

  1. 函数名相同,参数相同,但返回值不同的函数不算函数重载
short Add(short left, short right)
{
	return left+right;
}
int Add(short left, short right)
{
	return left+right;
}
// 不构成函数重载
  1. main函数不能重载。

  2. 全缺省函数和无参数的函数也可以构成函数重载,但是由于两者调用方式可以相同,会出现二义性,故不推荐这样使用。

    void print(int i = 10);
    void print()
    // 以上两个函数构成重载
    // 调用:
    print();
    // 具有二义性,编译器会报错
    
  3. 如果在重载函数调用时出现算术类型转换,也可能导致二义性。(算术转换不分级别,没有优先级,所以会出现二义性)

    void f(long);
    void f(float);
    
    f(3.14);// 3.14类型为 double 类型,它既能转换为long,也能转换为 float 。
    // 因为存在两者可能的算术类型转换,所以该调用具有二义性
    

4.2 重载和const形参


一方面,C语言中我们曾经学到过,顶层的const不影响传入的对象。因为顶层的const在调用时传给函数的是自身的拷贝,所以也就不会改变自身的值。

所以,一个拥有顶层const的形参无法与另一个没有顶层const的形参区分开来。

int add(int,int);
int add(const int,const int);// 重复声明了int add(int,int)

int swap(int*, int*);
int swap(int* const, int* const);// 重复声明了int swap(int*, int*)

另一方面,C++有一个新的类型,名字叫 引用 ,这里我们先不去探讨这是什么(在下一章就会详解),只需要知道引用传参相当于将自身传给了函数(也可以理解为将一个变量的地址传给了函数,在函数那里解引用)。如果形参是某种类型的指针或者引用,且它们的const为底层的,可以区分它们指向的是常量对象还是非常量对象以实现函数重载。

int swap(int&, int&);// 函数作用于int的引用
int swap(const int&,const int&);// 新函数,作用于常量的引用

int swap(int*, int*);// 函数作用于int的指针
int swap(const int*,const int*);// 新函数,作用于指向常量的指针

4.3 重载函数的调用过程


首先,我们来回顾一下,C语言的程序运行起来所要经历的过程:

【C++】C++入门特性全解析_第11张图片

如果重载函数是C++的特性,说明了C语言应该是不支持重载的,那么是不是这样呢?、

我们可以验证一下:

【C++】C++入门特性全解析_第12张图片

这是为什么呢?

【C++】C++入门特性全解析_第13张图片

  • 在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变

所以,

【C++】C++入门特性全解析_第14张图片

C++的编译器是怎么修饰函数名的呢?

我们以C++的一种编译器g++举例,g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】

【C++】C++入门特性全解析_第15张图片

通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

重载具体过程如下:

【C++】C++入门特性全解析_第16张图片

这里再补充一下函数重载调用的选择:

  • 如果没有找到可行函数,编译器将报告无匹配函数的错误。
  • 实参类型于形参类型越接近,它们匹配的越好。
  • 对于多个形参的函数匹配,匹配函数与实参要满足这样的关系:
    • 该函数每个实参都不劣于其他可行函数需要的匹配。
    • 至少有一个实参的匹配优于其他可行函数提供的匹配。
    • 如果不满足以上两个条件,可能会造成二义性。

4.4 extern “C”


有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加 extern "C",意思是告诉编译器,将该函数按照C语言规则来编译

由上文,我们知道C语言和C++的函数修饰规则不同,所以在相互使用对方的函数时,链接的过程会出现问题,这时候就需要 extern "C" 来帮忙。

当C++项目要调用C的库时:

【C++】C++入门特性全解析_第17张图片

C项目要调用C++库时:

【C++】C++入门特性全解析_第18张图片

__cplusplus 是C++才具有的常量定义,C是没有的,所以可以用它来作为编译条件。

总结一下:

【C++】C++入门特性全解析_第19张图片

无论什么项目要调用对方,都要在C++的项目中添加 extern "C"


5.引用


5.1 引用的定义


引用(Reference)是 C++ 相对于C语言的又一个扩充。引用可以看做是数据的一个别名通过这个别名和原来的名字都能够找到这份数据编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。

引用的使用格式为:type &name = data

eg.

int val = 1024;
int& rval = val;

引用与指针的声明方式类似,就是将前面的 * ,改为 &

引用的底层其实还是指针,这个知识点要记牢。


5.2 引用的特性


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

    	int val = 1024;
    	int& rval = val;
    	int& rval2;// 报错,引用必须被初始化
    

【C++】C++入门特性全解析_第20张图片

  1. 一个变量可以有多个引用。
	int val = 1024;
	int& rval = val;
	int& rval2 = val;
  1. 引用一旦引用一个实体,再不能引用其他实体

  2. 引用类型必须和被引用的对象类型相同

    	int val = 1024;
    	int& rval = val;// 正确
    	double& rval2 = val;// 错误,类型不匹配
    

5.3 引用与const


// 权限放大——不可以
const int a = 10;// 只具有可读权限
int& ra = a; // 该语句编译时会出错,a只有可读权限,不具有可写,但是引用将其权限放大
const int& ra = a;

int& b = 10; // 10是一个常量,常量都是只可读,不可写的,该语句编译时会出错
const int& b = 10;// 正确

// 权限缩小——可以
int c = 10;
const int rc = c;// 正确,缩小权限可正常使用

// 类型转换会产生临时变量(转换后的量),临时变量具有常性,没有可写的属性
double d = 12.34;
// 编译时出错,权限扩大,double->int发生类型转换
// 相当于rd引用了临时变量,扩大了权限,不可以使用
int& rd = d; 
const int& rd = d; // 正确

5.4 引用的函数传参


在C语言中,我们曾经学过:

  1. 结构体等空间占用比较大的类型一般使用其指针传参,好处是可以减少传参时形参拷贝所花费的时间。
  2. 有些函数就需要修改传入参数的值,比如swap函数,我们需要传递指针。

前文说过,引用的本质就是指针,所以前面说的指针的好处,引用都可以实现,而且要比指针使用起来方便。

eg.

struct A
{ 
    int b[10000]; 
}a;
void Func1(A a)
{
    cout << sizeof(a) << endl;
}
void Func2(A& ra)
{
    cout << sizeof(ra) << endl;
}
void Func3(A* pa)
{
    cout << sizeof(*pa) << endl;
}

// 函数调用
Func1(a);// 耗时过长,需要拷贝a
Func2(a);// 相当于直接给a起了一个别名ra,ra传递时,不需要拷贝a,因为引用只是给实体起了一个别名,别名不占空间
// ra的底层其实就相当于将a的地址传了过去,但是在函数内使用时可以直接使用别名,不需要解引用
Func3(&a);// 传递地址,使用需要解引用

总结:

  • 引用做参数:

    1、提高效率

    2、形参的修改,可以影响实参(输出型参数等)


5.5 引用作返回值


引用类型也可以作为函数的返回值:

eg.

【C++】C++入门特性全解析_第21张图片

但是这样是有非常大的bug的,上文说过引用和实体指向的其实是同一个空间,但是 c 是一个函数内的局部变量,出函数就销毁,空间也归还操作系统,操作系统可能会重置这片空间,所以 sum 的值就是一个不确定的量。

【C++】C++入门特性全解析_第22张图片

所以,引用作为返回值不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,但是它可以返回动态开辟的数据,静态数据以及全局的数据。

当然,引用作为返回值也可以修改返回值的值

int& getNum(int i)
{
	static int a[100] = { 0 };
	return a[i];
}

int main()
{
	// 写
	getNum(1) = 32;
	// 读
	cout << getNum(1) << endl;
	cout << getNum(0) << endl;

	return 0;
}

【C++】C++入门特性全解析_第23张图片

总结:

  • 引用作为返回值:
    • 不能返回局部变量。
    • 提高了传值的效率。
    • 可以修改返回值。

5.6 引用与指针的区别


虽然我们知道引用的底层就是指针,但是作为指针的改良,引用和指针也有许多不一样的点:

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

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

  3. 没有NULL引用,但有NULL指针

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

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

  6. 有多级指针,但是没有多级引用

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

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


6.内联函数


6.1 内联函数的定义


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

将函数指定为内联函数,通常就是将它在每一个调用点上“展开”,假如我们把Add函数指定为内联函数:

inline int Add(int left, int right)
{
	return a + b;
}

int ret = 0;
ret = Add(1, 2);
// 指定为内联后,编译时相当于
ret = 1 + 2;

内联函数的好处就是,减少函数压栈的开销,节约时间,提高效率。

我们来验证一下:

当Add函数没有指定为内联时:

【C++】C++入门特性全解析_第24张图片

可见调用了Add这个函数。

当Add函数指定为内联时:

【C++】C++入门特性全解析_第25张图片

没有调用Add,而是直接展开了。


6.2 内联函数的特性


  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜
    使用作为内联函数。

  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。

  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline在编译期间会被展开,所以 F 的符号表中就没有了 f 函数,也就就没有函数地址了,在其他文件中调用时,由于自己符号表中没有这个函数的地址,链接其他文件也没有这个函数的符号,就会找不到。

    // F.h
    inline void f(int i);
                   
    // F.cpp
    #include "F.h"
    void f(int i)
    {
    	cout << i << endl;
    }
                   
    // test.cpp
    int main()
    {
        f(1);// 
        return 0;
    }
    

7.C++11常用的特性


7.1 auto关键字


如果我们C语言的基础比较好,那么我们应该知道,C语言中就有一个关键字—— auto ,它的用处是:用它来修饰一个变量,就会建议编译器将这个变量用寄存器存储。

但是在C++11以后,auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

int a = 10;
auto b = a;// 编译器自动推导出来b的类型是int,所以相当于 int b = a

double c = 2.22;
auto d = a + c;// 由编译器自动推导 a + c 的类型

auto pa = &a;// 编译器自动推出,pa类型为int*
auto* pa = &a;// 编译器同样会推导出pa类型为int*
// auto在推导指针变量时,加*和不加*推导出来结果会相同

char ch = 'a';
auto ch2 = ch;// 编译器推导出ch2的类型为char

auto关键字的特性:

  1. 用auto声明指针类型时,用auto和auto*没有任何区别

    int a = 10;
    auto pa = &a;// 编译器自动推出,pa类型为int*
    auto* pa = &a;// 编译器同样会推导出pa类型为int*
    
  2. auto声明引用类型时则必须加&

    int a = 10;
    auto& ra = a; // 正确,ra类型为int&
    //auto ra = a; // 错误,a的类型是int,所以ra推导出来的类型是int,而不是int&
    // 因为引用和赋值的右值一般情况下都相同,所以auto声明引用类型时则必须加&
    
  3. 当在同一行声明多个变量时,这些变量必须是相同的类型

    auto i = 0, *p = &i;// 正确,i推导出来是int类型,p是整形指针
    auto sz = 0, pi = 3.14;// 错误,sz推导出来是int类型,而pi是double类型,类型不匹配
    // 因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。  
    
  4. 如果要让auto声明的变量推导出带有顶层const属性的类型,必须写为 const auto 变量名 = 数据 这样的形式。

    const int i = 10;
    auto a = i;// 此时虽然i具有const的属性,但是由于其为右值,const属性会被auto忽略,最后推导出来为 int
    const auto b = i;// 此时前面加了const修饰,最后b类型为const int
    
    // 当然我们也可以推导一下引用与const
    auto& c = 42;// 错误,推导出来c的类型是int&,但是42是一个常量,权限扩大
    const auto& d = 42;// 正确,推导出来c的类型是const int& 
    
  5. 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

    auto a;// 错误,没有初始化
    
  6. auto不能作为函数的参数

    int Add(auto a, auto b);// 错误,auto不能作参数
    
  7. auto不能直接声明数组

    auto a[] = {1, 2, 3};// 错误,auto不能直接声明数组
    

那么auto关键字我们一般在哪里使用呢?

  1. 类型名称非常长,手写非常麻烦时。
  2. auto在实际中最常见的优势用法就是跟待会会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

7.2 范围for语句


在C语言中,我们一定少不了遍历一个数组这样的操作:

int a[10] = {1, 2, 3};
for(int i = 0; i < sizeof(a); ++i)
{
	printf("%d ", a[i]);
}

这种方式不但繁琐,而且容易出错。

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号 “:” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

int a[10] = {1, 2, 3};
for(int e: a)
{
	// e 为 a 中的元素,随着循环向后迭代,与上文 a[i] 类似
	cout << e << " ";
}

通过上面的操作也可以遍历全部数组,而且方便快捷,不易出错,习惯上,我们一般把第一部分的类型写为 auto ,让其自行推导,这样就能写出一个无论什么类型都通用的写法:

for(auto e: array)
{
	// 无论 array 中元素类型是什么,都可以使用这个语句进行遍历
	cout << e << " ";
}

这个写法相当于:

for(int i = 0; i < sizeof(array); ++i)
{
	auto e = array[i];
 	cout << e << " ";
}

我们可以发现, e 的类型是 int , 但是如果我们要改变数组 array 中的元素呢?

for(int i = 0; i < sizeof(array); ++i)
{
 	// e的类型是int&,e就是array每个元素的引用
	auto& e = array[i];
    // 给每个元素加10
 	e += 10;
}

用范围for语句来写就是:

for(auto& e: array)
{
    e += 10;
}
  • 在范围for语句中,照常可以使用 continue 和 break 关键字。

范围for语句的特性:

  1. for循环迭代的范围必须是确定的
    对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
  2. 要想改变遍历对象的值,第一部分用于迭代的变量类型必须要为引用类型。

7.3 nullptt常量


nullptr 是一种特殊类型的字面值,C++11刚引入的一种方法,它可以被转换成任意其他的指针类型

在C语言中,我们给定一个 NULL 的宏来指定空指针。

int* p = NULL;

NULL 的宏定义:

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

我们发现在C++中, NULL 的定义就是0,在C语言中 NULL 的类型才为空指针

所以,在C++中使用 NULL 会出一些问题。

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);// 数字0,走第一个
	f(NULL);// C++中,NULL为0
	f((int*)NULL);
	return 0;
}

【C++】C++入门特性全解析_第26张图片

由上可得,只有在强行转换类型为指针时,NULL 才为指针。

我们再来试一试 nullptr

f(nullptr)

【C++】C++入门特性全解析_第27张图片

我们可以发现,nullptr 的类型就是指针。

注意:为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr


后记


这是一个新的专栏【C++】,白晨将在这里从入门到精通介绍C++相关的知识,希望可以帮助更多的人尽快上手C++。

众所周知,C++的语法是非常繁琐复杂,白晨会尽量将复杂的东西抽丝剥茧,比较清楚的讲解C++的各种语法,也会加上许多特性或者底层逻辑。白晨水平有限,如果哪里有问题或者想要和白晨探讨的同学,可以直接评论私信我,感谢大家的包容理解。

如果大家喜欢这个系列,还请大家多多支持啦!


如果这篇文章有帮到你,还请给我一个大拇指小星星 ⭐️支持一下白晨吧!喜欢白晨【C++】系列的话,不如关注白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

你可能感兴趣的:(C++,c++,开发语言,后端,c语言,github)