C++入门

文章目录

  • C++关键字(C++98)
  • C++命名空间
  • C++的输入和输出
  • 缺省参数(默认参数)
  • 函数重载
    • 函数重载概念
    • 名字修饰
  • 引用
    • 引用的概念
    • 引用的特性
    • 引用的使用场景
      • 1.引用做参数
      • 2.引用作为返回值
    • 值传递、引用传递效率比较
    • 常引用
      • 临时变量具有常属性
    • 引用和指针的区别
    • extern "C"
  • 内联函数
    • 内联的概念
    • 特性
    • 宏的优缺点
  • auto关键字(C++11)
    • auto的使用
    • 不能使用auto的场景
  • 基于范围的for循环(C++11)
    • 范围for的使用
    • 范围for的使用条件
  • 指针空值nullptr(C++11)
    • C++98中的指针空值
    • C++11中的空指针

本章是对C语言不足地方的改进。

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

C++总计63个关键字,C语言32个关键字。
现在,我们只是看一下C++有多少关键字,不对关键字进行具体解释,以后再慢慢学习。

asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast

C++命名空间

见C++命名空间。

C++的输入和输出

1.使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含 < i o s t r e a m > <iostream>头文件以及std标准命名空间;

2.cout和cin是全局的流对象,endl是特殊的C++符号,表示换行,他们都包含在头文件中;

3.<<是流插入运算符,>>是流提取运算符;

4.使用C++输入输出更方便,不需增加数据格式控制(printf、scanf需手动控制格式),比如:整形–%d,字符–%c。C++的cout能够自动识别类型;

5.cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载,后续再学习。

#include 

//using namespace std;//全部展开

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

int main()
{
	cout << "hello world" << endl;
	cout << "hello world\n";
	printf("hello world\n");

	int n = 10;
	double d = 1.5;
	
	//cout自动识别类型
	cout << n << " " << d << endl;
	
	printf("%.2lf\n",d);
	printf("%lf\n",d);//默认保留小数点后6位
	
	//从键盘获取
	std::cin >> n;
	cout << n << endl;
	
	const char* str = "hello world";
	cout<<str<<endl;
	return 0;
}

对于printf和cout,哪一个使用方便就用哪一个。

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

缺省参数(默认参数)

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

//缺省参数
void test(int a = 10)
{
	cout << a << endl;
}

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

全缺省

int add(int a = 10,int b = 20)
{
	return a + b;
}

int main()
{
	int ret = ();//30
	int ret2 = add(100,200);//300
	int ret3 = add(100);//120
	return 0;
}

半缺省
半缺省:部分参数缺省,并且必须从右向左缺省,必须连续缺省

//半缺省
void Fun(int a,int b = 20,int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

//void Fun(int a,int b = 20,int c)//错误

int main()
{
	Fun(10);
	Fun(10,1);
	Fun(1,2,3);
	return 0;
}

注意】:

  1. 半缺省参数必须从右往左依次来给出,并且不能间隔着给默认值
  2. 缺省参数不能在函数声明和定义中同时出现(语法就是这样规定的),要么写在声明,要么写在定义,建议写在声明中
  3. 缺省值必须是常量或者全局变量
  4. C语言不支持(编译器不支持)

函数重载

函数重载概念

一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

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

函数重载的要求满足下面一条即可
1.参数的类型不同
2.参数的个数不同
3.参数的顺序不同

//函数重载
void f(char a)
{
	cout << "f(char a)" << endl;
}

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

void f(int a,int b)
{
	cout << "f(int a,int b)" << endl;
}

//顺序不同
void f(char b, int a)
{
	cout << "f(char b,int a)" << endl;
}

1.缺省值不同,不能构成重载

void fun(int a = 10)
{
	cout << a << endl;
}

//不能构成重载
//void fun(int a)
//{
//	cout << a << endl;
//}

针对的是参数的类型,个数、顺序,这里类型相同,个数相同,顺序相同,所以不能构成重载。

2.能构成重载,但是存在问题

//构成重载,但是存在问题
//修饰后的函数名_Z1F
void F()
{
	cout << "F" << endl;
}

//修饰后的函数名_Z1Fi
void F(int a = 0) 
{
	cout << "F(int a)" << endl;
}

修饰后的函数名不同,这两个函数可以构成重载,但是在调用时,存在问题,F()到底是调用哪一个?

以下两个函数不能构成重载,参数类型,参数个数都相同,仅仅是参数名不同,并不能构成重载。

short Add(short left, short right)
{
	return left+right;
}

int Add(short left, short right)
{
	return left+right;
}

如果只是返回值不同,并不能构成函数重载(报错),因为调用的时候不能区分是哪个函数。

名字修饰

windows下,VS是根据文件后缀去调用对应的编译器,.c就是c编译器;.cpp就是c++编译器。

在Linux下不用文件后缀来区分编译器,gcc就是c编译器,g++就是c++编译器。

为什么C不支持函数重载,C++支持函数重载?

在此之前,我们学习过在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

C++入门_第1张图片
C++入门_第2张图片

1.预处理阶段:头文件在源文件中展开(展开后就没有头文件了)、宏替换、条件编译、删除注释(生成.i文件)

2.编译:检查语法、生成汇编代码(指令级语言代码)(生成.s文件)

3.汇编:汇编代码转换成二进制机器码(目标文件.o/.obj)、形成符号表(每个源文件都会生成一个目标文件,每个目标文件中都有符号表)

4.链接:合并段表,合并符号表和重定位,生成可执行程序(linux下默认为a.out)

下面,在Linux下,分析为什么C语言不支持函数重载,而C++支持函数重载。

C语言不支持函数重载的原因:在编译时,两个重载函数,函数名相同,符号表中就有两个相同的函数名,那么符号表中存在歧义和冲突,其次链接的时候也存在歧义和冲突,因为他们都是直接使用函数名去表示和查找,而重载函数的函数名相同,必然会有冲突。【在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。

C++入门_第3张图片

C++支持函数重载的原因:C++的目标文件符号表中不是直接用函数名来标识和查找函数,而是对函数名进行修饰。【在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。】

对于函数名的修饰规则,在不同的编译器下不同,在linux下,g++编译器中,函数修饰后变成【_Z+函数长度+函数名+形参类型的首字母**】**。

C++入门_第4张图片

有了函数修饰规则,只要参数不同,符号表里面重载的函数就不存在二义性和冲突了。

链接的时候,main函数里面去调用两个重载的函数,查找地址时,也是明确的。

注意:如果在当前文件有函数的定义,那么编译时就填上地址;如果当前文件只有函数声明,那么定义就在其他xxx.cpp文件中,编译的时候没有地址,只能在链接时去其他源文件的符号表中根据函数修饰名字去查找地址(声明的地址不是实际的地址,定义的地址才是真的地址),这就是链接的主要作用。

windows下函数名的修饰规则更加复杂,但是原理是相同的。可以阅读文章C/C++ 函数调用约定.

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

引用

引用的概念

因为C语言中指针的使用比较复杂,所以C++中引入了“引用”的概念。引用不是重新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

引用的使用方法:类型& 引用变量名(也就是要引用实体的别名) = 引用实体;

看下面代码:

C++入门_第5张图片
变量a和b的地址相同,改变a或者b的值,另一个变量的值也随之变化。
C++入门_第6张图片

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

引用在语法层:没有开新空间,就是给原来的变量(空间)取了一个新的名字。

引用的特性

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

	int a = 10;
	//int& b;//错误
	
	//引用在定义时必须进行初始化
	int& b = a;

引用在定义时必须进行初始化,因为你必须指明这个引用是哪个变量的别名。

2.一个变量可以有多个引用(一个变量可以有多个别名)

	int a = 10;
	int& b = a;
	//一个变量可以有多个引用(可以有多个名字)
	int& c = a;
	int& d = a;
	int& e = b;

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

	int a = 10;
	int& b = a;

	//引用一旦引用一个实体,就不能再引用其他实体
	int i = 100;
	b = i;//这里只是把变量i的值给b,并不是让b成为i的别名

指针是可以变的,可以指向这个变量,一会又可以指向另一个变量;但是引用不可以,引用从始至终只能是同一个变量的别名。

引用的使用场景

1.引用做参数

传参的三种方式:
1.值传递
2.址传递
3.引用传递

//址传递
//g++中修饰后的函数名为_Z4swappipi
void swap(int* a,int* b)
{
	//使用指针
	int tmp = *a;
	*a = *b; 
	*b = tmp;
}

//引用传递
//g++中修饰后的函数名为_Z4swapriri   r:reference引用
void swap(int& a,int& b)
{
	//1.引用作为形参,a是实参x的别名,b是实参y的别名
	//声明不会开空间,定义才会开空间
	//定义引用时必须进行初始化,但是这里形参是声明,声明引用是不需要初始化的
	int tmp = a;
	a = b;
	b = tmp;
}

//值传递
//g++中修饰后的函数名为_Z4swapii
void swap(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

//g++编译器中,三个函数修饰后的函数名不同,三个函数构成重载,
//但是swap(x,y);在调用时存在歧义,
//类似之前学习的void f();和void f(int a = 10);可以构成重载,但是调用时会存在歧义
//编译器不知道调用时传值还是传引用
int main()
{
	int x = 10;
	int y= 20;
	swap(&x,&y);
	//swap(x,y);//有歧义,到底是值传递还是引用传递,会报错
	return 0;
}

上面3个swap函数构成函数重载,引用和值传递看成不同的参数类型,但是swap(x,y)在调用时存在歧义,不能区分到底是值传递还是引用传递。

引用的底层是指针来实现,但是编译器编译时,并不会把引用替换成指针,转换成指令时才会替换成指针。在符号表中使用的是修饰后的函数名,但是使用的还是形参的类型,只要符号表中的函数名不同就能构成重载,也就是语法层面的参数类型不同即可,并不会关注底层是怎么实现的。

指针的引用

typedef struct SLTNode
{
	struct SLTNode* next;
	int data;
}SLTNode,*SLT;

//SLTNode的类型是struct SLTNode
//SLT的类型是struct SLTNode*

//指针的引用作为形参
//void SListInit(SLTNode*& phead)
void SListInit(SLT& phead)
{
	phead = NULL;
}


int main()
{
	int a = 10;
	//b是a的引用
	int& b = a;

	int* p1 = &a;
	//p2是p1的引用
	int*& p2 = p1;
	
	SLTNode s;
	SLT ps= &s;
	SLT& p = ps;
	SListInit(p);
	return 0;
}

在次之前,C语言部分学过输出型参数,在函数外部定义变量,然后传入该变量的地址,在函数内部改变该变量的值并返回,这样的参数是输出型参数。使用引用作为参数也可以实现该功能。

void change(int* pi)
{
	*pi = 100;
}


int main()
{
	int a = 10;
	change(&a);
	std::cout << a << std::endl;
	return 0;
}

使用引用作为参数:

void change(int& a)
{
	a = 100;
}


int main()
{
	int a = 10;
	//change(&a);
	change(a);
	std::cout << a << std::endl;
	return 0;
}

2.引用作为返回值

看如下代码,使用传值返回

int Add(int a,int b)
{
	int c = a + b;
	//传值返回,实际返回的是c的一份临时拷贝
	return c;
}

int main()
{
	int ret = Add(10,20);
	std::cout << ret << std::endl;

	return 0;
}

这里返回的并不是变量c,因为函数执行完毕,栈帧就销毁了,局部变量c我们就没有使用权限了(函数栈帧销毁之后,才会返回),即使能取到c的值(可能是正确的值,也可能是不正确的值,取决于栈帧销毁时清不清理空间),也是非法访问的。所以,为了解决这个问题,这里会生成一个临时变量tmp,把c的值拷贝给tmp,返回tmp,所以这里返回的是局部变量c的一份临时拷贝。

临时变量tmp存在哪里?
1.如果临时变量比较小(4或者8字节),一般是存储在寄存器中
2.如果临时变量比较大,临时变量存储在上一层函数的栈帧中(调用Add函数的函数栈帧中)。

值传递中形参是实参的一份拷贝,同样的,所有的传值返回都会生成一个拷贝。

使用引用返回

//引用传递返回
//但是会造成非法访问,函数调用结束后,栈帧销毁,c权限就返回给操作系统了
//我们再访问就是非法访问内存
int& Add(int a,int b)
{
	int c = a + b;
	return c;//返回引用的意思就是不会生成c的拷贝返回,而是直接返回c的引用
}

int main()
{
	int& ret = Add(10,20);//ret和局部变量c的地址相同
	//int ret = Add(10,20);//ret和局部变量c的地址不同

	//cout也会调用栈帧
	std::cout << ret << std::endl;//30

	//栈帧销毁后,调用别的函数就会重复利用销毁的空间,内存是可以重复使用的
	//但是只要不覆盖到上一次调用函数时存入栈帧的数据,就不会被改变

	Add(1, 2);

	std::cout << ret << std::endl;
	//值可能为30,要看内存有没有清理、有没有被覆盖

	return 0;
}

传值返回,会拷贝给临时对象,临时对象返回;引用返回的意思就是不会生成c的拷贝再返回,而是直接返回c的引用(可以理解为返回c的引用int& tmp)。

但是这段代码存在问题

1.存在非法访问内存,因为Add(10,20)的返回值是c的引用,所以Add栈帧销毁后,c已经销毁,再访问c就是非法访问内存;

2.如果Add函数栈帧销毁,并清理空间,那么取c值的时候取到的就是随机值,返回的也就是随机值,当然这个取决于编译器的实现(VS2019,对于malloc出来的空间,free释放后,一般会清理内存,函数调用结束后,栈帧销毁,不一定清理内存)。对于“非法写”编译器一般能检测出来,“非法读”编译器有时候检测不出来。

但是一般不建议使用引用返回,如果要使用,正确的使用方式如下:

如果函数返回时,出了函数作用域,如果返回对象还未还给系统(比如静态变量、全局变量等),则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。

int& Count()
{
	//static修饰变量,存在静态区
	static int n = 0;
	n++;
	// ...
	return n;
}

引用的价值:

1.做参数 – 提高效率;形参的修改可以改变实参(输出型参数)

typedef struct Stu
{
	int age;
	char name[30];
}Stu;

//指针做参数
void Func1(Stu* ps)
{

}

//值传递
void Func2(Stu s)
{

}

//引用做参数
void Func3(Stu& rs)
{

}

2.做返回值 – 提高效率;修改返回变量

//引用作为返回值
int& fun(int i) 
{
	static int a[10];
	return a[i];//没有产生空间,只是产生了一个临时变量int& tmp
}
int main()
{
	for (int i = 0;i < 10; i++)
	{
		fun(i) = 10 + i;
	}

	for (int i = 0; i < 10; i++)
	{
		std::cout << fun(i) << std::endl;
	}
	return 0;
}

传值返回,会拷贝给临时对象,临时对象被返回;
传引用返回,返回的是返回对象的引用(别名)。

不要返回局部变量的引用。

值传递、引用传递效率比较

传值返回和传引用返回的区别:传值返回会有拷贝;传引用返回没有拷贝。

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

以下代码测试值传递和引用传递的效率:

#include 
#include 
using namespace std;

struct A 
{ 
	int a[10000]; 
};

//值传参
void TestFunc1(A a) 
{

}

//引用传参
void TestFunc2(A& a) 
{

}

void TestRefAndValue()
{
	A a = { {0} };

	// 以值作为函数参数
	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;
}

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

运行这段代码,结果如下:
在这里插入图片描述
数据量非常大时,引用传递效率有显著提升。

总结:引用的作用主要体现在传参和传返回值

1.在有些场景下,可以提高性能。比如大对象+深拷贝对象

2.输出型参数和输出型返回值,也就是说,在有些场景下,形参的改变可以改变实参;有些场景下,引用返回,可以改变返回对象,后续学习再进行补充。

常引用

int main()
{
	const int a = 10;//a的值不能改变
	//int& b = a;//权限放大,因为a是只读的,但是b是a的引用,b是可读可写的,把权限放大了,错误

	const int& b = a;//正确,权限不变

	int c = 20;//c可读可写
	const int& d = c;//权限缩小,d只读,d是c的别名,d只读,是可以的
	return 0;
}

注意使用引用取别名的过程中,权限不能放大,权限可以不变或者缩小。

void f(int& x)
{
	std::cout << x << std::endl;
}

int main()
{
	const int a = 10;
	//f(a);//错误,因为a是只读的,但是f(int& x)中x是可读可写的,把权限放大了
	
	//int& b = a;//错误,b把权限放大了
	//f(b);
	return 0;
}
void f(const int& x)
{
	std::cout << x << std::endl;
}

int main()
{
	const int a = 10;
	const int& b = a;//b是a的引用
	f(b);//这里a,b,x共用一块空间
	return 0;
}

C++入门_第7张图片
这里,调用f(b)时,变量a,b,x共用同一块内存空间,b和x是a的引用。

//对于指针,权限的放大、缩小,指的是const放在*的前面
int main()
{
	const int* p1 = nullptr;
	int* p2 = nullptr;
	p1 = p2;//权限缩小
	//p2 = p1;//错误,权限放大

	//权限只能不变或者缩小,不能放大。
	return 0;
}

假设x是一个大对象或者是以后要学习的深拷贝的对象,那么尽量用引用传参,减少拷贝。如果f函数中不改变形参x,建议尽量用常引用来传参。

//使用常引用,在函数内部不能改变x的值
void f(const int& x)
{
	std::cout << x << std::endl;
}

int main()
{
	int a = 10;
	f(a);//权限缩小

	const int b = 20;
	f(b);//权限不变

	return 0;
}

临时变量具有常属性

int main()
{
	int a = 30;
	//相同类型没有隐式类型转换,不会产生临时变量
	int& b = a;
	const int& b1 = a;

	double d = 3.14;
	int c = d;//隐式类型转换

	//int& e = d;//错误

	const int& f = d;
	//类型不一致,这里有隐式类型转换,会产生一个临时变量来存储结果,并不是直接把d的值给f,f引用的是临时变量,f是临时变量的引用,而临时变量是具有常属性的,所以引用变量f的类型和临时变量保持一致,所以应该是const int&,引用权限只能不变或者缩小。

	return 0;
}

隐式类型转换:会产生一个临时变量(中间变量),把临时变量赋值给左值;整型提升、算术转换操作的都是临时变量,并不会改变原来的值。返回值也会产生临时变量。

f(a+c);//a+c会产生临时变量

不管是隐式类型转换还是强制类型转换,都会产生临时变量,临时变量具有常属性。

临时变量具有常性,不能被修改(临时变量是右值)。
常引用const Type&可以接收各种类型的对象。

引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用的实体共用同一块空间。但是在底层实现上,实际是有开辟空间的,因为在底层,引用是按照指针方式来实现的。

如下一段代码:

int main()
{
	int a = 1;
	int& b = a;

	int* p = &a;

	b = 2;

	*p = 3;
	return 0;
}

单步调试,查看汇编代码,我们发现引用和指针在底层实现是相同的
C++入门_第8张图片
语法层面
指针和引用是完全不同的概念,指针需要开辟空间来存储变量的地址,引用不需要开辟空间,仅仅是对变量取了一个别名。

底层汇编实现
引用是使用指针来实现的。

引用和指针的不同点:

  1. 引用在定义时必须初始化,指针没有要求(但是最好初始化)

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

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

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

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

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

  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理,我们直接使用引用变量即可访问

  8. 引用比指针使用起来相对更安全,指针使用起来更复杂、更不安全,指针要考虑空指针、野指针等。

void fun(int& a)
{
	a = 20;
}

void f(int* p)
{
	*p = 100;
}

int main()
{
	//fun(0);//错误,形参是int&,但是0是const类型
	//fun(NULL);//错误,没有空引用
	int i = 10;
	fun(i);

	//f(NULL);//空指针不能进行解引用操作
	//f(0);//0地址空间(也是空指针)我们没有权限访问

	return 0;
}

extern “C”

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

extern "C"只有C++能够认识,C语言不认识,所以extern "C"只能出现在C++代码中。

C++程序 调用C库,在C++程序中加extern “C”
C程序 调用C++库,在C++库中加extern “C”

//写法1
#ifdef __cplusplus//c++内置的标识符,c没有
extern "C"
{
#endif;
	int add(int a,int b);
#ifdef __cplusplus
}
#endif

如果在.cpp文件中,上面的代码等价于

//按照C语言编译规则来编译函数add
extern "C"
{
	int add(int a,int b);
}

如果在.c文件中,上面代码等价于

int add(int a,int b);
//写法2
#ifdef __cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif

EXTERN_C int add(int a,int b);

写法1和写法2的效果等价。

内联函数

内联的概念

为了解决C语言宏使用比较麻烦的问题,C++引入了内联函数。

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

int Add(int a,int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int n = 10;
	int m = 20;
	int sum = Add(n,m);
	return 0;
}

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间,编译器会用函数体替换函数的调用。

查看方式:

  1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add

C++入门_第9张图片

  1. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)

C++入门_第10张图片
C++入门_第11张图片

C++入门_第12张图片

总结】:长函数和递归函数不适合展开,展开后程序可能会变的很大。

特性

  1. inline是一种以空间换时间的做法,省去调用函数的开销。所以代码很长或者有循环/递归的函数不适合作为内联函数(短小的、频繁使用的函数建议设置成内联)。

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

3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

宏的优缺点

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

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

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

  1. 常量定义 换成用const
  2. 函数定义 换成用内联函数

auto关键字(C++11)

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储的局部变量,但是一直没有人去使用它,为什么?局部变量在函数调用结束后,栈帧就自动销毁,局部变量就不存在了,所以auto写不写都没有意义。

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时推导而得。

int main()
{
	//auto:根据右边的值,自动去推导左边变量的类型
	//a是整型
	auto a = 10;

	//c是字符类型
	auto c = 'w';
	//typeid打印变量的类型
	cout << typeid(a).name() << endl;//int
	cout << typeid(c).name() << endl;//char
	
	return 0;
}

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

auto的使用

1. auto与指针和引用结合起来使用

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

int main()
{
	int a = 10;

	auto p = &a;
	auto* b = &a;

	auto c = a;
	auto& d = a;

	cout << typeid(p).name() << endl;//int*
	cout << typeid(b).name() << endl;//int*
	cout << typeid(c).name() << endl;//int
	cout << typeid(d).name() << endl;//int

	return 0;
}
int main()
{
	const int a = 0;

	auto b = 10;
	cout << typeid(b).name() << endl;//int

	int c = 20;
	auto d = c;
	cout << typeid(d).name() << endl;//int	
	return 0;
}

2. 在同一行定义多个变量

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

void Test()
{
	auto a = 1, b = 2;
	auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

3.auto在实际中最常见的用法

auto最常用的就是跟以后会学习的C++11提供的新式for循环,还有lambda表达式等进行配合使用。

不能使用auto的场景

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

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

}

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

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

为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。

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

范围for的使用

在C++98中如果要遍历一个数组,可以按照以下方式进行:

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};

	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << endl;//i的生命周期是整个循环
	}

	return 0;
}

使用C++11中的范围for:

对于一个有范围的集合而言,由程序员来说明循环的范围,容易犯错误。因此C++11中引入了基于范围的for循环。

for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};

	for (int i : arr)
	{
		cout << i << endl;
	}
	return 0;
}

再结合auto一起使用

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};

	for (auto i : arr)
	{
		cout << i << endl;
	}
	return 0;
}

基于范围的for循环(又叫语法糖)使用起来非常方便。

注意以下情况:

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};

	for (auto e : arr)
	{
		//局部变量e自身++,不会改变数组每个元素的值
		e++;
	}

	for (int i : arr)
	{
		cout << i << endl;
	}//1 2 3 4 5 6 7 8 9 10

	return 0;
}

若要改变数组元素的值,使用引用:

int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,10};
	
	for (auto& e : arr)
	{
		//这里e的生命周期和C++98中i的生命周期不同
		e++;//e的生命周期是一次循环
	}
	return 0;
}

范围for的使用条件

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

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

//下面代码错误,for循环范围不知道
void TestFor(int array[])
{
	//array是指针
	for(auto& e : array)
		cout<< e <<endl;
}

int main()
{
	int array[] = {1,2,3,4,5,6,7,8,9,10};
	TestFor(array);//错误,数组名是首元素地址,形参会退化为指针,形参array是一个指针
	return 0;
}

2. 迭代的对象要实现++和==的操作

对于这两点,以后再详细学习。

指针空值nullptr(C++11)

C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,它就是野指针,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
	int* p1 = NULL;
	int* p2 = 0;
	// ……
}

在C++中,NULL实际是一个宏,查看其定义:
C++入门_第13张图片
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如

void f(int)
{
	cout << "f(int)" << endl;
}

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

int main()
{
	f(0);//f(int)
	f(NULL);//f(int)
	f((int*)NULL);//f(int*)
	return 0;
}

f(0)和f(NULL)都调用了f(int)函数,其实我们希望f(NULL)调用f(int*),但是NULL定义成了0。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量*,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。所以在C++11中,引入了新的关键字nullptr作为空指针。

C++11中的空指针

注意

1.nullptr是C++11中引入的关键字,在使用nullptr表示指针空值时,不需要包含头文件;C语言中是没有nullptr的。

2.在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

3.为了提高代码的健壮性,以后表示指针空值时建议最好使用nullptr,不要使用NULL。

你可能感兴趣的:(C++初阶,引用,内联函数,函数重载)