c++ - 第1节 - c++入门

目录

1.C++关键字(C++98)

2. 命名空间

3.C++输入&输出

 4.缺省参数

4.1.缺省参数概念

4.2.缺省参数分类

5. 函数重载

5.1.函数重载概念

5.2.名字修饰(name Mangling)

5.3.extern “C”

6. 引用

6.1 引用概念

6.2.引用特性

6.3.常引用

6.4.使用场景

6.5.传值、传引用效率比较

6.6.引用和指针的区别

7. 内联函数

7.1.概念

7.2.特性

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

8.1.auto简介

8.2.auto的使用细则

8.3.auto的意义

8.4.auto不能推导的场景

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

9.1.范围for的语法

9.2.范围for的使用条件

10. 指针空值nullptr(C++11)


1.C++关键字(C++98)

C++总计63个关键字,C语言32个关键字

下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲


2. 命名空间

1.c++中的#include<iostream>与c语言中的#include<stdio.h>类似,包含IO流,是用来和控制台进行输入输出的。(cout、endl包含在了iostream中,stdio.h里面是没有的)

c语言中包含头文件是#include<stdio.h>这种形式;c++包含头文件可以是#include<stdio.h>形式,也可以是#include<cstdio>,就是头文件名把.h去掉在前面加一个c,两者的区别是cstdio有命名空间而stdio.h没有命名空间

2.命名冲突:一种情况,我们要使用rand作为一个变量的变量名,如果不包含stdio.h头文件的话,程序可以正常通过,如果包含了stdio.h头文件,那么就会提示重定义,因为stdio.h中rand是一个库函数,这就造成了命名冲突,但是语法只规定变量名不能和关键字相同,没有规定变量名不能和库函数名相同。另一种情况,两个人合作写工程代码,一个人定义了一个变量名a,另一个人定义了一个函数名a,这样也会造成命名冲突。也就是定义一个全局变量名,有可能会和库里面函数名或自定义函数名冲突,也有可能和自定义全局变量名冲突

c语言没有很好的解决这个问题,cpp引入namespace解决这个问题

3.在同一个域里面不能有同名的变量名或函数名,如果有同名的变量名或函数名就会造成命名冲突。比如,定义两个全局变量名或两个函数名f或一个全局变量名一个函数名,就会造成命名冲突,而一个全局变量名或函数名和一个局部变量名是不会造成命名冲突的。因此我们使用namespace关键字,建立一个命名空间域,与外面的全局变量名或函数名隔离开,就不会造成命名冲突了。

namespace建立的命名空间域,不同的人使用不同命名的命名空间域即可(只要命名空间域名字不同并且同一域里面的名字不冲突就没有问题)如下图一行左所示。

命名空间域中不仅可以定义变量还可以定义函数、结构体类型等,如下图一行右所示

命名相同的两个命名空间是可以同时存在的,在代码运行的时候会被合并成一个,如果命名相同的两个命名空间中有命名相同的变量名,最后代码将两个命名空间合并的时候里面命名相同的变量名就会冲突,解决办法是可以进行命名空间嵌套,如下图二行图所示

命名空间是可以进行嵌套的,嵌套几层访问的时候使用几个::即可,如下图二行图所示

命名空间只是用来进行隔离的,不影响里面变量的生命周期,并且命名空间不能在函数里面定义,所以命名空间里面的变量为全局变量,如下图三行图所示

      

4.如下图所示,我们知道相同的变量名,局部变量优先,所以打印出来为1,而如果我们想打印全局变量a,我们只需要在a的前面加 “ 空格::  ,那么访问的就是全局的a。::是域作用限定符,作用是指定右边的变量名是在左边域中的变量名,空格默认为全局域。空格::a就是访问全局域中的变量a(::a左边也可以不加空格,左边不加空格那么左边就没有是空白的,也认为全局域,加上空格更加形象)

5.有一个函数f和一个命名空间域中的变量f,在主函数中如果只写一个f那么访问的是函数f,要想访问命名空间域中的变量f,使用 bit::rand 即可,如下图所示

6.如果命名空间多次嵌套,每次定义都需要从外往里写命名空间名很麻烦,使用

using namespace+命名空间名 可以把命名空间名对应这个命名空间定义的东西放出来,相当于没有该层命名空间了,如下图第一二行所示。这里不能先展开data再展开byte,如下图三行所示,是错误的,因为byte是在外面的。如果写成using namespace byte::cache,那么展开的只是cache命名空间,byte命名空间没有展开

7.如下图所示,每次使用变量f和rand,前面都需要加 bit:: 这样很麻烦。第一种方法是我们前面提到的加一句using namespace bit进行展开,后面用到f变量直接写f即可,但是这样做的话会把rand也放出来,使用rand变量如果包含了stdlib.h,那么就会和rand库函数冲突。我们可以用什么展什么,可以using bit::f只放f出来,这样使用f变量时直接写f,使用rand变量时,写bit::rand。因此using的使用有两种方式:

(1)using namespace+命名空间名   将命名空间名对应的命名空间全部释放出来

(2)using 命名空间名 :: 变量名  只放某个变量出来

8. c++代码我们可以看到前面很多时候会这样写using namespace std,是为了解封std命名空间,这样后面使用里面的变量前面就可以不用写std::了,如下图一行左所示,这样放出来,方便使用了,但是存在冲突风险,如果再定义一个全局变量cout就会造成变量名冲突,如下图一行右所示。

如果不加using namespace std的话,那么所有使用std里面的变量前面都需要加std:: ,但是这样就不会再访问冲突了,如下图三行所示,这体现了命名空间的优势

这里我们可以只把常用的放出来,例如放cout出来,使用using std::count,那么所有的cout前面可以不用再加std::,如下图三行左所示,这里如果有全局变量也叫cout,同样会造成访问冲突,如下图三行右所示。

  

 

   


3.C++输入&输出

1.流提取运算符: >>   

        cin >> a ( 与scanf("%d",&a)效果相同 )

2.流插入运算符: <<   

        cout << a( 与printf("%d",a)效果相同 )

注:

1.流提取运算符和流插入运算符可以自动识别类型并且可以一行输入输出多个,提取时数字间还是以空格或回车分割

2.输出时如果要换行,那么就在后面加一个endl或'\n'

3.下面代码中cout << "hello world" << endl是提取一个字符串再提取一个换行

4.c++是兼容c语言的,如果想控制格式输出,比如控制小数点后位数输出,可以用printf函数

    printf函数、scanf函数、>>、<<是可以混着用的


 4.缺省参数

4.1.缺省参数概念

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

4.2.缺省参数分类

1.全缺省参数

注:传参时传了第一个参数才能传第二个参数,顺序进行,c++不支持第一个参数使用默认值,对第二个参数传参这种情况

2.半缺省参数

注:

1.半缺省的情况下,至少要传缺省部分的参数

2.半缺省必须是从右往左缺省(缺省参数是指给了参数默认值),并且是连续的(从左往右缺省或中间缺省是有歧义的,如果传一个或几个参数不知道参数到底是从左往右依次传还是传给没有默认值的变量)

3.半缺省实用示例,这样如果使用者知道数据量较大,第一次就可以控制开100个空间,避免多次扩容且使用更加灵活

4.缺省参数(半缺省参数或全缺省参数)不能在函数声明和定义中同时出现,否则会报重定义默认参数错误,如下图所示(c++设计者怕使用者两个默认参数给的不同,系统不知道该选哪个,存在歧义)

如果声明和定义分离的话,我们必须是在声明里面给默认值,定义里面不给默认值,或者两个都不给默认值,这样代码才能正常运行(定义里面给默认值,声明里面不给是不行的,因为在编译阶段,包含的头文件已经进行展开,此时在test.i文件中展开的声明里面没有默认值,如果使用函数时不传参使用默认值,编译阶段系统会报错)


5. 函数重载

5.1.函数重载概念

函数重载:是函数的一种特殊情况,C++允许在 同一作用域中声明几个功能类似 的同名函数,这些同名函数的 形参列表(参数个数 或 类型 或 类型的顺序)必须不同(和变量名无关),常用来处理实现功能类似数据类型不同的问题

注:

1.上面提到形参列表顺序不同,其中顺序指的是变量类型的顺序而不是变量名的顺序,如下图所示。如果只是变量名顺序不同,不构成重载,程序无法运行

2.c语言不支持重载,那么我们如果要交换两个整型变量的值和两个浮点型变量的值,需要用两个函数名,这样很不舒服;c++支持了重载,那么我们可以都叫swap函数即可,使用时更加方便

3.如果只有返回值类型不同,形参列表(参数个数 或 类型 或 类型的顺序)相同,这样无法区分要调用的是谁,不构成重载,程序无法运行,如下图所示

  

5.2.名字修饰(name Mangling)

为什么C++支持函数重载,而C语言不支持函数重载呢?下面我们进行讲解

1.我们用linux操作系统创建一个项目,并创建test.cpp、f.h、f.cpp文件,如下图所示

2.编译链接的过程:

对文件 f.h   f.cpp   test.cpp进行编译

(1)预处理——头文件展开、宏替换、条件编译、去掉注释

        生成文件:f.i   test.i

(2)编译——检查语法、生成汇编代码(代码有语法错误就是在这个阶段检查出来的)

        生成文件:f.s   test.s

(3)汇编——把汇编代码转换成二进制的机器码

        生成文件:f.o   test.o

(4)链接——找调用函数的地址,链接对应上,合并到一起

在编译汇编完成之后,每一个.o文件都会对应生成一个函数调用指令和一个符号表,符号表会记录函数名和函数地址的映射,如下图所示

           

在编译汇编的时候,因为还没有链接所以test.o的main函数指令中我们不知道f.o符号表中函数定义处的地址,但是经过预处理f.h头文件已经展开了,前面有函数的声明,函数的声明相当于承诺其他文件中有这个函数,此时有了承诺,编译汇编过程也可以正常通过,到后面链接的时候,系统会对照f.o符号表,将test.o main函数指令问号处函数地址补齐。

如果开始的f.cpp文件中没有函数定义而f.h中有函数声明,那么编译能通过,但是最后链接的时候在其他文件符号表中找不到两个f函数的地址,系统就会报链接错误

3.C++支持函数重载,而C语言不支持函数重载的原因

· c语言中,如果有两个相同函数名的函数,那么在编译时生成符号表,符号表里面两个函数名相同,如下图所示。从图中可以看出,符号表里面的函数名和原函数名相同(因为c语言不能重载,所以我们只运行一个函数,观察符号表里的函数名是否有变化),因此系统无法区分符号表中两个函数,编译不通过。因此c语言不支持函数重载

· c++中,如果有两个相同函数名的函数,那么在编译时生成符号表,符号表里面两个函数名不相同,如下图所示。从图中可以看出符号表里面新的函数名中把参数类型的首字母加了进去,这就意味着如果形参列表(参数个数 或 类型 或 类型的顺序)不同,那么即使原函数名相同,符号表里的函数名也不会相同这样原本两个相同函数名的函数在符号表中的函数名不再相同,系统可以区分两个函数,编译通过。后面链接的时候,系统会对照f.o符号表,将test.o main函数指令问号处函数地址补齐,因此c++支持函数重载

从上图中我们可以看出linux系统下,符号表中对函数重新命名是以_Z + 函数名长度 + 函数名 + 类型首字母 的方式进行命名的(与变量名是什么,返回值是什么没有关系)

linux操作系统下运行文件是不会解析其后缀名的,如果我们用g++对上述.cpp文件进行编译,那么就会进行c++识别编译,如果用gcc对上述.cpp文件进行编译,那么就会进行c语言识别编译。windows操作系统下运行文件会解析其后缀名,如果后缀为.cpp默认进行c++编译,如果后缀为.c默认进行c语言编译。

5.3.extern “C”

在c语言中,可以调c语言的静态库/动态库,也可以调c++的静态库\动态库

在c++中,可以调c++的静态库/动态库,也可以调c语言的静态库\动态库

也就是说c语言和c++可以交叉调用

我们深入分析可以发现一个问题,如果c语言调用c++静态库/动态库的话,在编译那一步的时候会生成符号表,c++静态库/动态库生成符号表,符号表中的函数名是新的函数名与原函数名不同,而我们是c语言运行主函数代码,主函数所在文件在编译阶段生成的符号表和main函数指令与原函数相同,这样有函数声明的情况下编译正常通过。在链接的时候main函数指令中要在其他符号表中找的函数名因为c++符号表中函数名重新命名而无法找到,因此链接失败,程序无法运行。

问题:那么c语言和c++如何才能交叉调用呢?

解决方法:extern “C”,也就是说extern “C”的作用是使c语言可以调用c++库,c++可以调用c语言库

1.首先我们制作一个c语言静态库/动态库

(1)重新建立一个项目,选择空项目,我们将项目命名为Stack_C。然后将之前写的Stack.c文件和Stack.h文件拷贝到新项目中,拷贝流程之前介绍过,先复制两个文件,然后右键源文件/头文件选择添加现有项,将两个文件粘贴在弹出的对话框中,点击添加,最后将.h和.c分别拖到头文件和源文件栏中即可

 

(2)右键项目名称选择属性,弹出的属性页面中配置属性栏常规中有一个配置类型,选择静态库,然后应用确认即可。运行代码,这样我们在工程文件夹的debug子文件夹中就能找到Stack_C.lib静态库,同时工程文件夹中Stack_C子文件夹中还有Stack.c和Stack.h文件

 

2.在c++环境下调用刚刚建立的c语言静态库

(1)新创建一个项目工程,创建cpp代码环境,我们找一道之前做过的需要用到栈的题,我们使用括号匹配题目的代码,拷贝该代码,此时代码已经有了,我们需要使用刚刚封装好的静态库。

(2)右键创建的c++文件选择打开所在文件夹,从此文件夹寻找前面静态库工程文件夹中Stack_C子文件夹的Stack.h文件,将寻找路径写在#include" "里的双引号中进行声明(其中 .. 表示跳到上层目录)。

(3)此时运行程序,我们发现会报链接错误,这是因为我们要调用刚刚的静态库,还需要对主函数所在项目进行配置,右击项目名称选择属性,弹出的属性框链接器栏的常规中,附加库目录选择编辑,弹出的附加库目录对话框中点击新行按钮,在里面找到静态库所在的debug文件夹,选择添加该debug文件夹点击确认并应用确认。并且在属性页链接器栏的输入中,附加依赖项中将静态库的名字加进去,并且用分号和其他名字隔开,应用确定。

(4)配置完后,我们运行程序,发现还是报链接错误,此时如果我们把Stack.c文件改成Stack.cpp文件,运行Stack.cpp程序生成静态库,然后运行主函数所在程序,程序可以正常运行,说明c++调c++没有问题,而c++链接c静态库链接那一块还是有问题。

(5)分析上面的问题,cpp程序无法链接c语言的静态库是因为c++编译时生成的符号表函数名会更新变化,而c语言的话编译时生成的符号表函数名不会改变。这样c++函数指令要找的函数是变化后的函数当然在c语言符号表中无法找到。我们使用extern"C"将#include声明括起来,这样就是告诉编译器,extern"C"声明的函数,是C库,要用c的方式去链接调用,如下图所示,这样我们就成功完成了c++程序调用c的静态库

1.首先我们制作一个c++静态库/动态库

(1)重新建立一个项目,选择空项目,我们将项目命名为Stack_CPP。然后将之前写的Stack.cpp文件和Stack.h文件拷贝到新项目中,拷贝流程是,先复制Stack.c文件和Stack.h文件两个文件,然后右键源文件/头文件选择添加现有项,将两个文件粘贴在弹出的对话框中,点击添加,最后将.h和.c分别拖到头文件和源文件栏中,将Stack.c后缀名修改成.cpp即可

(2)右键项目名称选择属性,弹出的属性页面中配置属性栏常规中有一个配置类型,选择静态库,然后应用确认即可。运行代码,这样我们在工程文件夹的debug子文件夹中就能找到Stack_CPP.lib静态库,同时工程文件夹中Stack_CPP子文件夹中还有Stack.cpp和Stack.h文件

2.在c语言环境下调用刚刚建立的c++语言静态库

(1)新创建一个项目工程,创建c语言代码环境,我们找一道之前做过的需要用到栈的题,我们使用括号匹配题目的代码,拷贝该代码,此时代码已经有了,我们需要使用刚刚封装好的c++静态库。

(2)右键创建的c语言文件选择打开所在文件夹,从此文件夹寻找前面静态库工程文件夹中Stack_CPP子文件夹的Stack.h文件,将寻找路径写在#include" "里的双引号中进行声明(其中 .. 表示跳到上层目录)。

(3)此时运行程序,我们发现会报链接错误,这是因为我们要调用刚刚的静态库,还需要对主函数所在项目进行配置,右击项目名称选择属性,弹出的属性框链接器栏的常规中,附加库目录选择编辑,弹出的附加库目录对话框中点击新行按钮,在里面找到静态库所在的debug文件夹,选择添加该debug文件夹点击确认并应用确认。并且在属性页链接器栏的输入中,附加依赖项中将静态库的名字加进去,并且用分号和其他名字隔开,应用确定。

(4)配置完后,我们运行程序,发现还是报链接错误,c语言程序无法链接c++的静态库是因为c语言编译时生成的符号表函数名不会变化,而c++的话编译时生成的符号表函数名会更新变化。这样c语言函数指令要找的函数是原本名字的函数当然在c++符号表中无法找到。解决办法还是extern"C",此时我们在c++静态库.h文件中每个函数声明前面都加上extern"C",这里每个函数声明前面都加上extern"C"是告诉c++编译器在编译的时候符号表中函数名采用c语言的方式生成,但是只这样做的话还是不行,因为预编译将.h文件里面的声明在c编译器展开后,c编译器不认识extern"C",此时我们要在c++静态库.h文件中用到条件编译,当编译器是c++的话用c++方式编译,extern"C"进行修饰,当编译器是c语言的话,不再进行extern"C"修饰,问题得到解决

注:

1.extern"C"加在函数声明的前面也可以括起来多个函数声明

2.c++静态库.h文件中,条件编译和extern"C"上面的写法如下图左所示,还有一个写法可以不用在每个声明前都加extern"C",如下图右所示,两种写法都可以

  

3.如果主函数main使用c语言编译,调用c++静态库,c++静态库中不能有函数重载。如果静态库有函数重载,静态库编译将无法通过,因为我们使用了条件编译,.h文件函数声明我们用c语言的的方式生成的符号表,生成的符号表名字会冲突

如果c++静态库中有重载,那么只能将重载函数名改为不同的函数名

上面演示所用到的代码如下所示

c语言静态库:

stack.h文件:

#pragma once

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

//struct Stack
//{
//	int a[N];
//	int top; // 栈顶的位置
//};

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶的位置
	int capacity;	// 容量
}ST;

void StackInit(ST* ps);
void StackDestory(ST* ps);
void StackPush(ST* ps, STDataType x);
void StackPop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
STDataType StackTop(ST* ps);

stack.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void StackDestory(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	// 
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->a = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
		if (ps->a == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		ps->capacity = newCapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}

void StackPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

bool StackEmpty(ST* ps)
{
	assert(ps);

	/*if (ps->top > 0)
	{
		return false;
	}
	else
	{
		return true;
	}*/
	return ps->top == 0;
}

STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}


int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

c++静态库:

stack.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶的位置
	int capacity;	// 容量
}ST;

#ifdef __cplusplus
extern "C"
{
#endif
	void StackInitN(ST* ps, int n);
	void StackInit(ST* ps);

	void StackDestory(ST* ps);
	void StackPush(ST* ps, STDataType x);
	void StackPop(ST* ps);
	bool StackEmpty(ST* ps);
	int StackSize(ST* ps);
	STDataType StackTop(ST* ps);

#ifdef __cplusplus
}
#endif

stack.cpp文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void StackDestory(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	// 
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->a = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
		if (ps->a == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		ps->capacity = newCapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}

void StackPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

bool StackEmpty(ST* ps)
{
	assert(ps);

	/*if (ps->top > 0)
	{
		return false;
	}
	else
	{
		return true;
	}*/
	return ps->top == 0;
}

STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}


int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

主函数所在文件:

test.c / test.cpp文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "../../Stack_CPP/Stack_CPP/Stack.h"

bool isValid(char* s)
{
	ST* B = (ST*)malloc(sizeof(ST));
	StackInit(B);
	while (*s)
	{
		if (*s == '(' || *s == '[' || *s == '{')
		{
			StackPush(B, *s);
			s++;
		}
		else
		{
			if (StackEmpty(B))
			{
				return false;
			}

			char top = StackTop(B);
			StackPop(B);
			if ((top == '[' && *s != ']') || (top == '(' && *s != ')') || (top == '{' && *s != '}'))
			{
				return false;
				StackDestory(B);
			}
			else
			{
				s++;
			}

		}
	}
	bool ret = StackEmpty(B);
	StackDestory(B);
	return ret;
}


int main()
{
	char a[] = "[]{}";
	printf("%d\n", isValid(a));


	return 0;
}


6. 引用

6.1 引用概念

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

注:

1.取了别名后,别名和原名的地址是同一块空间

2.引用有如下作用,这里对形参变量引用,那么r1就是x的别名,r2就是y的别名,因此将r1和r2交换,也就是将x和y交换

3.引用也可以给指针变量取别名,如下所示

6.2.引用特性

1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体

6.3.常引用

1.取别名原则:对原引用变量,权限只能缩小,不能放大

2.对常量取别名 和 类型转化下取别名

(1)不能直接对一个常量取别名,前面加const才可以对一个常量取别名

(2)像 double d = 2.2; int f = d ;这样的隐式类型转换,中间会产生一个临时变量,临时变量具有常性,不能被修改,因此int& e = d错误不是因为类型不同,而是因为中间产生的临时变量具有常性,如果用int& e来接收相当于权限放大,因此无法接收,而用const int& e来接收权限不变,是可以的。并且,这里面e引用的不是d,而是中间产生的那个常变量,也就是e是中间常变量的别名

注:可以 int f = d 而取别名的话必须 const int& e = d。这里是因为int f = d的话是将d生成一个中间常变量,将中间常变量赋值给f,f改变不会影响中间常变量,f可以改变;const int& e = d是将d生成一个中间常变量,e取中间常变量的别名,如果没有const,这里e改变中间常变量会跟着改变,而中间常变量是无法被改变的,所以必须加const

6.4.使用场景

1. 做参数
可作为输入型参数,也可作为输出型参数(输出型参数之前介绍过,给一个指针参数,你将这个参数解引用赋值后进而拿到某些值,相当于另一种方式的输出)
引用做参数优势:减少拷贝,提高效率
注:
1.使用引用传参时,要注意取别名原则:对原引用变量,权限只能缩小,不能放大。如下图所示,如果形参是int& x,实参为10、c、d、e都传不了,这些都涉及到了权限的放大,将形参改为const int& x,实参为10、c、d、e才能够传。
  

2. 做返回值
传值返回会有一个拷贝,传引用返回没有这个拷贝了,函数返回的直接就是返回变量的别名
引用做返回值优势:减少拷贝,提高效率
思考下面代码:

注: 
1.如下图左所示,虽然函数的返回值是int类型,但是不能用int& ret来接收,因为函数的返回值首先会被拷贝保存在寄存器或一块内存空间中,拷贝的变量是一个临时变量,临时变量具有常性,涉及到权限的变大,因此应该用const int& ret来接收。这里ret是临时变量的别名。
  

2.如果返回类型是引用类型,如下图一行代码所示,那么中间临时变量tmp类型就是int& ,然后将临时变量赋值给ret。这里本质其实并没有开辟空间,因为int&类型的tmp是变量n的别名,取别名不会开辟新的空间。

下面一行这种代码是有问题的,相当于用引用搞出了野指针,出了函数局部变量n已经被释放了,但是通过别名ret仍记着变量n的这块内存地址。只有当对局部变量n被static修饰时,才能这样使用来减少拷贝,如下图第二行代码所示,因此引用做返回值有下面注意点3

  

3.总结:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回或传值返回,如果已经还给系统了,则必须使用传值返回。

6.5.传值、传引用效率比较

以值作为参数类型,在传参期间,函数不会直接传递实参,而是传递实参的一份临时的拷贝,因此用值作为参数类型,效率是非常低下的,尤其是当参数类型非常大时,效率就更低。

以值作为返回值类型,在返回期间,函数不会直接将变量本身直接返回,而是返回变量的一份临时的拷贝(出了函数,函数栈帧销毁,函数内局部变量自动释放,所以需要拷贝保存)(拷贝的时候,如果返回值占用内存不大,就会临时保存在寄存器中;如果返回值占用内存大,编译器会提前在上一层函数栈帧中提前开好内存,临时保存在此处开好的内存中),因此用值作为返回值类型,效率是非常低下的,尤其是当返回值类型非常大时,效率就更低。

6.6.引用和指针的区别

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

引用和指针相比:

1.语法角度而言:引用是取别名,没有额外开空间;指针是存储地址,开辟4/8字节空间

2.底层角度而言:他们是一样的方式实现的(转换成相同的汇编代码)


7. 内联函数

宏相比于函数,宏直接在调用位置展开,不会创建栈帧

为什么c++要出inline内联函数?

解决了宏函数晦涩难懂,容易写错的问题,也决解了宏无法调试(内联函数debug下支持调试,因为inline没起作用,所以可调试)和没有类型安全检查的问题

7.1.概念

inline修饰的函数叫做内联函数, 编译时C++编译器会在 调用内联函数的地方展开,没有函数压栈的开销, 内联函数提升程序运行的效率。
注:
1. (1)在release模式下,C++编译器会在调用内联函数的地方展开(查看编译器生成的汇编代码中是否存在call Add,没有call Add就是会展开)
    (2)在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)

2.在debug模式下,需要对编译器进行设置才会展开,否则不展开,因为编译器默认不会对代码进行优化,以下是vs2019的设置方式:

 右击项目名字选择属性,属性框中在c/c++栏中选择常规,将调试信息格式改为程序数据库,应用确认。属性框中在在c/c++栏中选择优化,将内联函数扩展改为只适用于_inline,应用确认。此时再进行调试反汇编,可以看到已经没有call了,在debug模式下进行了展开

 

7.2.特性

1. inline是一种 以空间换时间的做法,省去调用函数的开销(调用函数的开销就是建立栈帧),但是多次调用时内存占用空间很大(因为不用inline时多次调用建立栈帧,每次出了函数栈帧会销毁,内存可以重复利用;而使用inline时,多次调用每一次都会展开,不会重复利用空间,空间消耗很大)。所以代码很长(一般是10行左右取决于编译器)或者有循环/递归的函数不适宜使用作为内联函数。
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline是进行展开,符号表里面没有地址,链接的时候在其他符号表里面找不到,所以链接错误。
如果声明和定义都放在F.h文件中(如果都放在F.cpp,主函数所在文件对F.cpp不进行包含,所以还是有问题,链接错误),那么主函数所在test.cpp文件进行包含,就会将声明和定义都包含进来,就可以进行展开,不会出现问题。


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

8.1.auto简介

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

auto功能:自动推导变量类型

注:

1.使用 typeid(变量名).name( ) 返回一个字符串,字符串里面是变量的类型

2.使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

8.2.auto的使用细则

1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

8.3.auto的意义

auto的意义一:类型很长时,懒得写,可以让它自动推导

auto的意义二:范围for

下面代码auto e:array的功能是依次自动取数组array中的数据,赋值给e,自动判断结束

注:

1.auto e:array的功能是依次取array数组中的数据赋值给e,e是数组中值的拷贝,e的改变不会影响数组中的值,如下图所示

如果想用e对数组中的值进行改变,那么我们可以取别名,这样e就不再是数组中值的拷贝,而是数组中值的别名,这样就可以对数组中的值进行修改,如下图所示

其实范围for不一定非得用auto,自己手动去匹配数组中的元素类型也是可以的,并且变量名不一定非得用e,使用其他变量名也可以,用如下图所示

8.4.auto不能推导的场景

1.auto不能作为函数的参数

下面代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导

void TestAuto(auto a)
{
}

2.auto不能作为函数的返回类型

下面代码编译失败,auto不能作为返回类型,返回类型必须明确指定

3. auto不能直接用来声明数组 
下面代码编译失败,auto不能用来直接声明数组
void TestAuto()
{
 int a[] = {1,2,3};
 auto b[] = {4,5,6};
}


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

9.1.范围for的语法

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

 

注:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

9.2.范围for的使用条件

1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
下面代码就有问题,因为for的范围不确定。此处传参后array不再表示整个数组,而是是一个指针,指向数组首元素地址
2. 迭代的对象要实现++和==的操作 (关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)


10. 指针空值nullptr(C++11)

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
 int* p1 = NULL;
 int* p2 = 0;
 
 // ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,在c++里面,NULL被定义成了0

c++由于早期的失误,把NULL定义成了0,就导致了下图所示代码的问题,程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。 c++11中为了补这个坑,就增加了nullptr,nullptr被定义成(void *)0,是空指针

注:

1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

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

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值