初识c++


前言

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。

一、C++命名空间

1、命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
例如在如下代码中会报出rand重定义的错误,因为rand在stdlib库中为函数名。如果将rand定义在main函数内,则不会出现冲突,因为stdlib.h中的rand函数的作用域为全局,而main函数中的rand作用域为main函数内,所以在main函数中访问rand会就近原则去访问main作用域内的rand变量,就不会去全局再找rand函数了。而如果将rand设置为全局变量,当在main函数中访问rand时,此时全局作用域中有一个rand变量,还有一个rand函数,就会出现冲突。

#include<stdio.h>
#include<stdlib.h>
//rand定义为全局变量会产生冲突
int rand = 10;
int main()
{
	//rand定义为局部变量不会冲突
	int rand = 10;
	//因为stdlib库中定义了rand()函数,所以全局定义的rand会和库中的rand产生命名冲突
	//在c语言中没办法解决类似这样的命名冲突的问题,所以c++提出了namespace来解决

	printf("%d\n", rand);

	return 0;
}

c++的namespace就可以解决c语言中的命名冲突问题。

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

//命名空间域
namespace dong
{
	int rand = 0;
}

int main()
{

	printf("%d\n", rand);  //此时rand为stdlib内的函数

	//  ::为域作用限定符
	printf("%d\n", dong::rand);  //此时dong::rand为dong命名空间域里面的rand变量

	return 0;
}

2、命名空间定义

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

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

//命名空间域
namespace dong
{
	//命名空间中定义变量
	int rand = 0;

	//命名空间还可以嵌套定义
	//嵌套定义命名空间
	namespace inside
	{
		int tmp = 5;
		struct Stu
		{
			char name[20];
			int age;
		};
	}

	//命名空间中定义函数
	void func()
	{
		printf("func()\n");
	}

	//命名空间中定义结构体
	struct TreeNode
	{
		struct TreeNode* left;
		struct TreeNode* right;
		int val;
	};
}

int main()
{

	printf("%d\n", rand);  //此时rand为stdlib内的函数

	//  ::为域作用限定符
	printf("%d\n", dong::rand);  //此时dong::rand为dong命名空间域里面的rand变量

	//此时会去全局域找func()函数,如果全局域没有定义func()就会报错
	//func();  

	//dong::指定了去命名空间域dong里面找func()的定义。
	dong::func();  

	//dong::指定了去命名空间域dong里面找struct TreeNode的定义。
	struct dong::TreeNode node;

	//访问嵌套定义的命名空间里面的变量
	printf("%d\n", dong::inside::tmp);

	struct dong::inside::Stu stu;

	return 0;
}

当我们在一个项目中,如果有多个模块都用到了类似栈和队列的操作,并且它们也定义了Stack和QueueNode结构等,此时我们可以直接使用一个namespace dong的命名空间将该模块的.h和.c文件都包含在namespace dong命名空间域中,此时在namespace dong里面的变量和结构体定义就不会和别的模块里面的变量和结构体产生冲突了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
此时当我们想使用命名空间dong中定义的结构体和方法时,就不能再想下面图片中那样定义了。因为一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
在这里插入图片描述
此时使用dong命名空间的结构体和函数就可以如下图一样的形式使用。这样就可以避免每个模块之间命名冲突,比如这个模块使用这些操作的话就去dong命名空间中去找这些定义,这个模块再使用其他操作的话可以去另一个命名空间中去找这些定义,这两个命名空间中可以定义相同名字的结构体或变量或函数,只要在使用时标明去哪个命名空间去找这些结构体或变量或函数的定义即可。
在这里插入图片描述

二、第一个c++程序

1、c++的hello world

下面为c++语言写的第一个hello world程序。

#include<iostream>
using namespace std;
int main()
{
	cout << "hello world" << endl;

	return 0;
}

其中该语句说明引入了c++里面的io流的库。

#include<iostream>

该语句说明将std命名空间里面的成员都引入,c++中标准库的东西都放到了std中,即如果有了该语句,在下面的代码中使用std里面的变量时就不需要写成std::cout,而是可以直接写成cout,省略前面的std::。因为此时std命名空间的成员已经都引入到全局域了,所以编译器可以在全局域里面找到这些成员的定义。

using namespace std;

如果我们将using namespace std;注释掉的话,再使用命名空间std里面的成员时,就需要像下面这样写。

#include<iostream>
//using namespace std;
int main()
{
	std::cout << "hello world" << std::endl;

	return 0;
}

我们还可以只引入std库里面部分的变量名称。

#include<iostream>
//using namespace std;
//只引入我们需要的成员即可
using std::cout;
using std::cin;
using std::endl;
int main()
{
	cout << "hello world" << endl;

	return 0;
}

2、std命名空间的使用惯例

1.在日常练习中,建议直接using namespace std即可,这样就很方便。
2 using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 或 using std::cout展开常用的库对象/类型等方式。

#include<iostream>
#include<vector>
//引入常用的库对象/类型
using std::cout;
using std::endl;
int main()
{
	//指定命名空间访问
	std::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;
	cout << "hello world!" << endl;

	return 0;
}

三、C++输入&输出

1、c++输入&输出

1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
3. << 是流插入运算符,>> 是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
在c语言中,当我们需要在标准输出流stdout中打印数据时,使用printf函数,当我们要读取数据时,需要使用scanf从标准输入流stdin中获取数据,并且这两个函数在打印和获取数据时,都要标明数据的类型。
但是c++中不需要像c语言那样标明数据类型。

#include<iostream>
//using namespace std;
int main()
{
	//  << 是流插入运算符
	std::cout << "hello world" << std::endl;

	//std命名空间中的endl就类似于换行符,即 std::endl 等价于 "\n"
	std::cout << "hello world" << "\n";

	//c++中自动识别类型
	int i = 11;
	double d = 11.11;
	printf("%d %lf\n", i, d);
	//c++中可以自动识别类型
	std::cout << i << "," << d << std::endl;

	//c语言中读取数据
	scanf("%d %lf", &i, &d);
	printf("%d,%lf\n", i, d);

	//c++中读取数据
	//  >> 为流提取
	std::cin >> i >> d;
	std::cout << i << "," << d << std::endl;

	//当需要精度控制时,因为c++实现精度控制比较麻烦,所以还可以使用c语言的printf()
	printf("%.2lf\n", d);

	return 0;
}

四、c++中缺省参数

1、缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。缺省值必须是常量或者全局变量。

//缺省参数
#include<iostream>
using namespace std;
void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
	//当调用函数Func()时,传递的有实参,则缺省参数的值不起作用
	Func(1);
	Func(2);
	Func(3);

	//当调用函数Func()时,没有传递实参,此时缺省参数的值就起作用
	Func();

	return 0;
}

2、缺省参数分类

全缺省参数:全缺省参数需要注意的就是在函数调用时,不能直接跳过前面的形参,然后传值给后面的形参。

//全缺省参数
#include<iostream>
using namespace std;
void TestFunc(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	
	TestFunc();
	//传入的实参是按照从左向右的顺序赋值的,只有一个实参就赋给形参a
	TestFunc(1);
	//有两个实参就赋给a和b
	TestFunc(1,2);
	//有三个实参就赋给a、b、c
	TestFunc(1,2,3);

	//这里需要注意的是,不能直接跳过前面的a,然后直接传值给b
	//TestFunc(, 1, );  //这样是错误的,语法不允许
	return 0;
}

半缺省参数:这里需要注意的是半缺省参数必须从右往左依次来给出,不能间隔着给。

//半缺省参数
#include<iostream>
using namespace std;

//这里函数TestFunc只定义了两个缺省参数,所以在调用时必须要将形参a的值通过实参传递过来
//半缺省参数必须从右往左依次来给出,不能间隔着给
//   void TestFunc(int a = 10, int b, int c = 30)  //这样定义的就是错误的
void TestFunc(int a, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	//此时必须要将形参a的值通过实参传递过去
	//TestFunc();  //错误
	// 
	//传入的实参是按照从左向右的顺序赋值的,只有一个实参就赋给形参a
	TestFunc(1);
	//有两个实参就赋给a和b
	TestFunc(1, 2);
	//有三个实参就赋给a、b、c
	TestFunc(1, 2, 3);

	return 0;
}

3、缺省参数应用

前面我们使用c语言实现了栈的创建和初始化,当我们初始化栈时会为栈的容量开辟4个空间,但是当我们知道需要向栈中插入多少数据时,此时如果还每次只开辟capacity*2的空间,然后一次一次开辟到我们需要的空间时,这样程序的开销就大了,因为每次开辟空间还有可能需要将数据转移。而此时缺省参数就可以解决上面的问题,我们将栈容量定义为缺省参数,当在初始化栈时,如果已经提前知道了需要多少空间,我们就可以给capacity传入值,而当不知道需要多少空间时,就可以不用给capacity传值,此时capacity就为4。

#include<iostream>
#include<stdlib.h>
struct Stack
{
	int* a;
	int top;
	int capacity;
};
//将栈的容量capacity设为缺省参数
void StackInit(struct Stack* ps, int capacity = 4)
{
	//当调用StackInit()没有给capacity传参时,缺省参数的值就起作用,此时capacity为4
	//当调用StackInit()给capacity传参时,此时缺省参数的值就不起作用,capacity的值就为传进来的值
	ps->a = (int*)malloc(sizeof(int) * capacity);
	//
	ps->top = 0;
	ps->capacity = capacity;

}

int main()
{
	//当我们在使用栈时,已经提前知道了一定会插入100个数据,所以可以直接将capacity的值当作实参传入
	//这样可以提前开好空间,插入数据时就避免了扩容,程序开销就会小一些
	struct Stack st1;
	StackInit(&st1, 100);

	//当我们不知道需要插入多少数据时,就可以不传入capacity的值。
	struct Stack st2;
	StackInit(&st2);
}

上面的代码中我们是直接将StackInit(struct Stack* ps,int capacity=4)函数写出来了,而在项目中,我们需要在.h文件中先将StackInit(struct Stack* ps,int capacity=4)声明一下,然后在.c文件中完成StackInit(struct Stack* ps,int capacity=4)函数的定义。此时就会产生一个问题,如果StackInit()函数的声明和定义中给缺省参数capacity的值不一样时,那么编译器会以哪个为准呢?所以缺省参数不能在函数声明和定义中同时出现
此时编译器就不知道该以哪一个为准了。
在这里插入图片描述
在这里插入图片描述
所以都是在函数声明中给缺省参数值,然后函数定义时就不需要给缺省参数值了。
在这里插入图片描述
在这里插入图片描述
那如果在函数定义时给了缺省参数,而在函数声明时没有给缺省参数,那么此时缺省参数的值不会起作用。因为已经在函数声明时说明了该函数没有缺省参数,但是调用时按缺省参数的形式调用,就会出现错误。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、c++中函数重载

1、函数重载概念

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

//函数重载 参数个数不同
#include<iostream>
using namespace std;
int Add(int a, int b, int c)
{
	return a + b + c;
}
int Add(int a, int b)
{
	return a + b;
}

int main()
{
	cout << Add(2, 2, 2) << endl;
	cout << Add(2, 2) << endl;

	return 0;
}

函数重载 – 参数类型不同

//函数重载 参数类型不同
#include<iostream>
using namespace std;
int Add(int a, int b)
{
	return a + b;
}
double Add(double a, double b)
{
	return a + b;
}

int main()
{
	cout << Add(2, 2) << endl;
	cout << Add(1.1, 2.2) << endl;

	return 0;
}

函数重载 – 参数类型顺序不同

//函数重载 参数类型顺序不同
#include<iostream>
using namespace std;
void func(int i, char ch)
{
	cout << "void func(int i, char ch)" << endl;
}
void func(char ch, int i)
{
	cout << "void func(char ch, int i)" << endl;
}
int main()
{
	func(1, 'a');
	func('a', 1);

	return 0;
}

当函数返回值不同时不构成函数重载,只有参数不同才构成重载。因为函数返回值不同,函数调用时编译器无法区分去调用哪一个函数。
在这里插入图片描述

2、函数重载应用

在c语言中,当我们要交换两个int型变量的值时,需要写一个函数;当我们要交换两个double类型的值时,又要写一个函数,而且两个函数的名字还不能相同。

#include<stdio.h>
void Swapi(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void Swapd(double* p1, double* p2)
{
	double tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

int main()
{
	int a = 1;
	int b = 2;
	double c = 1.1;
	double d = 2.2;
	Swapi(&a, &b);
	Swapd(&c, &d);

	printf("%d %d\n", a, b);
	printf("%lf %lf\n", c, d);
	return 0;
}

但是c++中的函数重载就可以解决这个问题,只要两个名称相同的函数的参数数量、类型或顺序不同,就可以构成函数重载。

#include<iostream>
using namespace std;
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void Swap(double* p1, double* p2)
{
	double tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

int main()
{
	int a = 1;
	int b = 2;
	double c = 1.1;
	double d = 2.2;
	Swap(&a, &b);
	Swap(&c, &d);

	cout << a << " " << b << endl;
	cout << c << " " << d << endl;
	return 0;
}

通过上述代码我们可以发现c++中的cout和cin可以自动识别类型,其实也是用到了函数重载,这样cout和cin才可以在各种类型时都可以用。
在这里插入图片描述

六、c++中的引用

1、 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用变量的定义: 类型& 引用变量名(对象名) = 引用实体;

//引用
#include<iostream>
using namespace std;

int main()
{
	int a = 0;
	//b为a变量的别名,a和b共用一块内存空间,不管修改a或b的值,这片空间的值都会改变
	int& b = a;

	//a和b共用一块内存空间,所以a和b的地址一样
	cout << &b << endl;
	cout << &a << endl;

	a++;
	b++;
	cout << a << endl;
	cout << b << endl;

	return 0;
}

在这里插入图片描述

2、引用特性

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

//引用特性
#include<iostream>
using namespace std;

int main()
{
	int a = 1;

	//1.引用在定义时必须初始化,不然就不知道该引用是哪个变量的别名
	//int& b; 

	//2.一个变量可以有多个引用,即这些变量都和a共享一片内存空间,只要其中一个引用的值改变了,该内存空间的值就改变了
	int& b = a;
	int& c = a;
	//还可以给a的别名c再起一个别名,此时d也和c一样,与a共享一片内存空间。
	int& d = c;
	++a;
	++b;
	++c;
	++d;
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;

	int x = 10;
	//3.引用一旦引用一个实体,就不能再引用其他实体
	b = x;  //因为此时是给b赋值为x,并不是将b变为x的别名
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;

	return 0;
}

3、常引用

当定义一个const修饰的int型变量时,则说明该变量的值不可以被修改,如果此时给该变量设置一个int类型的引用,然后通过该引用将这个常变量的值修改时,这就违反了const修饰的变量的值不能被修改的规则。所以在创建引用时,也需要加上const修饰。

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;
	//typeid().name()可以显示该变量的类型
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;

	const int c = 20;
	//c变量被const修饰,即为常变量,权限为只能读取,不能修改
	//而int& d = c;将c的权限放大为可读可修改了,所以会报错。
	//int& d = c; 
	//当将d的权限也设置为只能读取,不能修改,此时const int& d = c;发生权限平移,不会报错
	const int& d = c;

	//权限可以缩小
	//e变量的权限为可读可修改
	int e = 30;
	//f为只能读,不能修改,所以const int& f = e;发生了权限缩小,不会报错
	const int& f = e;

	
	return 0;
}

所有类型转换都不是对原类型的数据进行改变,而是生成一个临时变量,将原类型的值拷贝到其中,再对该临时变量进行类型转换。因为如果将int类型强制类型转换为double类型,这是没办法操作的,int占4个字节,double占8个字节,如果改变int的值,根本没有办法存储的下8个字节,所有此时会创建一个8个字节的临时变量,然后拷贝int的值,再将该值转为double类型。

#include<iostream>
using namespace std;
int main()
{
	int ii = 1;
	double dd = ii;
	//将int型的ii强制类型转换为double类型,并不会再ii的地址内将ii的值改变,
	//而是创建一个临时变量,然后将ii的值给临时变量,将该临时变量的值转为double类型。
	//double dd = (double)ii;
	// 类型转换,并不会改变原变量类型,中间都会产生一个临时变量

	//发生了权限放大,会报错
	//double& rdd = ii;
	//这样就没有发生权限放大了,不会报错
	//此时rdd为临时变量的别名,其与临时变量共享一片空间,而不是和ii共享一片空间
	const double& rdd = ii;

	
	const int& x = 10;  //x为常量10的别名


	return 0;
}

在这里插入图片描述
如果使用引用传参时,函数内如果不改变实参的值,那么建议尽量用const引用传参,因为该形参具有很强的接受度。

#include<iostream>
using namespace std;
void func1(int n)
{
	//该函数的形参为实参的拷贝,所以在函数中修改形参n的值,并不会影响到实参的值,所以常量可以当作实参传进来
}
void func2(int& n)
{
	//该函数的形参为实参的别名,在函数中可以通过该别名修改实参的值,所以常量不可以被当作实参传递过来
}
void func3(const int& n)
{
	//如果使用引用传参,函数内如果不改变实参的值,那么尽量使用const修饰引用传参
	//因为这样定义时,形参具有很强的接受度。
}
int main()
{
	int a = 10;
	const int b = 20;
	func1(a);
	func1(b);
	func1(30);  //func1()函数对实参是只读,所以没有发生权限放大

	func2(a);
	//func2(b);
	//func2(c);   //func2()函数对实参为读写,而本来b和30为常量,只有读的权限,所以发生了权限放大,会报错

	func3(a);
	func3(b);
	func3(30);
	func3(1.11);  
	double d = 2.22;
	func3(d);  //此时double类型也可以传过去,因为传的是类型转换时产生的临时变量

	return 0;
}

4、指针和引用的区别

引用和指针的不同点:

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

5、引用的应用 – 做参数

(1)输出型参数
引用的一个使用场景就是作为输出型参数,即在函数定义形参时,将形参定义为实参的引用,然后就可以通过引用来改变实参的值。

//引用应用
#include<iostream>
using namespace std;
//形参是实参的别名,即引用,所以可以通过形参来改变实参的值
void Swap(int& r1, int& r2)
{
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}
int main()
{
	int a = 0;
	int b = 2;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	Swap(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	return 0;
}

所以我们可以使用引用来修改一下之前写的顺序表,我们还记得之前写的顺序表代码如下。

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;  //指向动态开辟的数组
	int size;  //有效数据个数
	int capacity;  //顺序表的容量
}SL;

void SLInit(SL* ps)
{
	assert(ps);
	SLDataType* tmp = (SLDataType*)calloc(4, sizeof(SLDataType));  //初始化时自动开辟4个空间
	if (tmp == NULL)
	{
		perror("SLInit");
		exit(-1);  //退出程序
	}
	ps->data = tmp;
	ps->size = 0;
	ps->capacity = 4;
}

int main()
{
	//创建一个SL类型的结构体变量
	SL s;
	//需要将s地址传入函数,这样函数中才能通过s的地址来改变s结构体变量的值
	SLInit(&s);

	return 0;
}

当我们了解了引用之后,我们就可以将上面的代码改为使用引用来写,而不需要使用指针。

#include<iostream>
#include<stdlib.h>
#include<assert.h>
using namespace std;

typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;  //指向动态开辟的数组
	int size;  //有效数据个数
	int capacity;  //顺序表的容量
}SL;

//形参是实参的别名,即引用,所以可以通过形参来改变实参的内容。
void SLInit(SL& ps)
{
	assert(&ps);
	SLDataType* tmp = (SLDataType*)calloc(4, sizeof(SLDataType));  //初始化时自动开辟4个空间
	if (tmp == NULL)
	{
		perror("SLInit");
		exit(-1);  //退出程序
	}
	ps.data = tmp;
	ps.size = 0;
	ps.capacity = 4;
}

int main()
{
	//创建一个SL类型的结构体变量
	SL s;
	//此时直接将该变量传入函数中
	SLInit(s);


	return 0;
}

那么我们前面写的单链表也可以改成使用引用的版本,我们之前写的单链表代码如下。在实现单链表的一些操作时,我们使用到了二级指针。

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

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个新结点
	assert(newNode);
	newNode->data = x;
	newNode->next = NULL;
	if (*pphead == NULL)  //如果单链表为空,就使新结点为单链表的首结点。
	{
		*pphead = newNode;
	}
	else
	{
		//找单链表的尾结点
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newNode;
	}
}

int main()
{
	//创建一个单链表结点指针,并将该指针赋值为NULL,代表该单链表为空
	SLTNode* s = NULL;
	//此时如果函数SListPushBack()中想要改变s的值不为NULL,就需要将s的地址传入函数中,
	//而s本来就为指针,所以SListPushBack()函数的形参要定义一个二级指针,用来接收指针s的地址。
	SListPushBack(&s, 1);
	SListPushBack(&s, 2);
	SListPushBack(&s, 3);
	SListPushBack(&s, 4);

	return 0;
}

现在我们可以使用引用来改变上面的代码,这样我们就不用使用二级指针了。

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

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//将函数的形参定义为一个SLTNode*类型的指针变量的引用
//此时pphead就是list的别名,phead的改变也会影响list
void SListPushBack(SLTNode*& pphead, SLTDataType x)
{
	assert(&pphead);
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个新结点
	assert(newNode);
	newNode->data = x;
	newNode->next = NULL;
	if (pphead == NULL)  //如果单链表为空,就使新结点为单链表的首结点。
	{
		pphead = newNode;
	}
	else
	{
		//找单链表的尾结点
		SLTNode* tail = pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newNode;
	}
}

int main()
{
	//创建一个单链表结点指针,即代表该单链表为空
	SLTNode* list = NULL;
	//此时SListPushBack(SLTNode*& pphead, SLTDataType x)的形参为list指针变量的引用
	//所以在函数中通过该引用就可以改变list的内容
	SListPushBack(list, 1);
	SListPushBack(list, 2);
	SListPushBack(list, 3);
	SListPushBack(list, 4);

	return 0;
}

在一些数据结构的书上也会这样定义,即将上面的代码再次简化。

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

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode,*PSLTNode;

//*PSLTNode等价于下面的语句
//typedef struct SListNode* PSLTNode;


//将函数的形参定义为一个SLTNode*类型的指针变量的引用
//void SListPushBack(SLTNode*& pphead, SLTDataType x)
//上面的语句等价于下面的语句
void SListPushBack(PSLTNode& pphead, SLTDataType x)
{
	assert(&pphead);
	SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个新结点
	assert(newNode);
	newNode->data = x;
	newNode->next = NULL;
	if (pphead == NULL)  //如果单链表为空,就使新结点为单链表的首结点。
	{
		pphead = newNode;
	}
	else
	{
		//找单链表的尾结点
		SLTNode* tail = pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newNode;
	}
}

int main()
{
	//等价于SLTNode* s;
	//通过PSLTNode创建一个单链表结点指针,该指针为空,就说明该单链表为空
	PSLTNode s = NULL;
	
	SListPushBack(s, 1);
	SListPushBack(s, 2);
	SListPushBack(s, 3);
	SListPushBack(s, 4);

	return 0;
}

(2)大对象传参,提高效率

#include<time.h>
#include<iostream>
using namespace std;
struct A
{
	int a[10000];
};

void TestFunc1(A aa)
{

}

void TestFunc2(A& aa)
{

}

int main()
{
	A a;

	//以值作为函数参数
	int begin1 = clock();
	for (int i = 0; i < 10000; ++i)
	{
		//每次调用TestFunc1函数都会在该函数中重新创建一个A结构体,并且该结构体的数据都拷贝实参a的值
		TestFunc1(a);
	}
	int end1 = clock();

	//以引用作为函数参数
	int begin2 = clock();
	for (int i = 0; i < 10000; ++i)
	{
		//调用函数TestFunc2不会重新创建A结构体,所以该循环会效率更高
		TestFunc2(a);
	}
	int end2 = clock();

	cout << end1 - begin1 << endl;
	cout << end2 - begin2 << endl;
	
	return 0;
}

6、引用的应用 – 做返回值

(1)输出型返回对象
我们知道c语言中每当有一个函数调用时,编译器都会在栈区中为该次函数调用开辟一片空间,例如有如下一个程序。Count()函数的返回值为int型,那么函数调用时是如何将返回值带回到main函数内的呢?

我们先看下面两个传值返回的例子。
(1)我们可以看到当Count函数执行完后,编译器为该函数分配的空间就会回收,此时存储返回值n的空间也销毁,那么正确的n的值是怎么回到main函数中的呢?其实当Count函数返回值较小时,此时系统会直接将Count函数的返回值存在寄存器中,然后main函数去该寄存器取返回值即可。当Count函数返回值较大时,会先在main的栈帧中创建一个临时变量,然后将Count栈帧里面的n拷贝到main的临时变量中,不会直接取Count函数的栈帧中取n,因为此时Count函数的栈帧已经销毁,n的内存空间中已经存的不是原来的值了。
在这里插入图片描述
(2)下图中的n为static修饰的静态变量,所以此时n存储在静态区。此时虽然函数Count的空间销毁后,n还是存在静态区中没有被销毁,但是在main函数中获取Count函数的返回值时也不会去静态区去取n的值。因为编译器不知道此时静态区中有Count函数的返回值。而是还会将Count的返回值和上面例子的处理情况一样,即将Count函数的返回值拷贝一份到main函数的临时变量中。
所以不管n是在栈区还是静态区,编译器都会生成一个函数返回对象的拷贝,用来作为函数调用返回值,这样调用函数的栈帧销毁了,返回值也还有拷贝的一份,不会让函数的返回值丢失。
在这里插入图片描述

我们再看下面的两个传引用返回的例子。
下面的例子中main函数中的ret的值是不确定的,所以这种情况不能使用传引用返回。只能使用传值返回。
在这里插入图片描述
下面的例子中就可以使用传引用返回,因为返回值n在静态区中,Count函数执行时的空间被系统销毁后,n的值还在。所以当出了函数作用域,返回对象就销毁了时,那么一定不能用传引用返回,一定要用传值返回,只有在出了函数作用域后,返回对象还没有销毁的情况时才可以使用传引用返回。而传值返回不管什么情况都可以使用,因为传值返回会将返回值拷贝一份。传引用返回就是比传值返回少了一次返回值的拷贝。
在这里插入图片描述
知道了上面的知识后,我们又可以将以前写的关于顺序表的修改数据的函数改一下,改为传引用返回的函数。原来我们写的顺序表的修改数据的代码如下。

#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;  //指向动态开辟的数组。
	int size;  //有效数据的个数
	int capacity;  // 容量空间的大小
}SL;
void SLInit(SL* ps)
{
	assert(ps);
	SLDataType* tmp = (SLDataType*)calloc(4, sizeof(SLDataType));  //初始化时自动开辟4个空间
	if (tmp == NULL)
	{
		perror("SLInit");
		exit(-1);  //退出程序
	}
	ps->data = tmp;
	ps->size = 0;
	ps->capacity = 4;
}


void SLCheckCapacity(SL* ps)
{
	assert(ps);
	if (ps->size == ps->capacity && ps->size != 0)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->data, (2 * ps->capacity) * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("SLCheckCapacity");
			exit(-1);
		}
		ps->data = tmp;
		ps->capacity = ps->capacity * 2;
	}
}


void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);  //先检查是否需要扩容
	ps->data[ps->size] = x;  //插入数据
	ps->size++;
}

void SLModify(SL* ps, int pos, SLDataType x)
{
	assert(ps && pos >= 0 && pos <= ps->size);
	ps->data[pos] = x;
}

int main()
{
	SL s;
	SLInit(&s);
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);
	SLModify(&s, 2, 6);
	return 0;
}

然后我们可以将SLModify函数改为一个传引用返回的函数,这样我们就可以根据返回的别名,来修改顺序表中元素的值。

#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
#include<iostream>
using namespace std;
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* data;  //指向动态开辟的数组。
	int size;  //有效数据的个数
	int capacity;  // 容量空间的大小
}SL;
void SLInit(SL& ps)
{
	assert(&ps);
	SLDataType* tmp = (SLDataType*)calloc(4, sizeof(SLDataType));  //初始化时自动开辟4个空间
	if (tmp == NULL)
	{
		perror("SLInit");
		exit(-1);  //退出程序
	}
	ps.data = tmp;
	ps.size = 0;
	ps.capacity = 4;
}


void SLCheckCapacity(SL& ps)
{
	assert(&ps);
	if (ps.size == ps.capacity && ps.size != 0)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps.data, (2 * ps.capacity) * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("SLCheckCapacity");
			exit(-1);
		}
		ps.data = tmp;
		ps.capacity = ps.capacity * 2;
	}
}


void SLPushBack(SL& ps, SLDataType x)
{
	assert(&ps);
	SLCheckCapacity(ps);  //先检查是否需要扩容
	ps.data[ps.size] = x;  //插入数据
	ps.size++;
}

int& SLModify(SL& ps, int pos)
{
	assert(&ps && pos >= 0 && pos <= ps.size);
	return ps.data[pos];
}

int main()
{
	SL s;
	SLInit(s);
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPushBack(s, 4);

	for (int i = 0; i < s.size; ++i)
	{
		cout << SLModify(s, i) << " " ;
	}
	cout << endl;
	//此时返回的就是s.data[2]的别名,通过该别名就可以修改s.data[2]的值
	SLModify(s, 2)++;
	SLModify(s, 3) = 10;
	for (int i = 0; i < s.size; ++i)
	{
		cout << SLModify(s, i) << " ";
	}
	cout << endl;

	//虽然也可以直接s.data[2]来修改,但是这个例子就是举例说明一下传引用返回的应用
	s.data[2]++;
	for (int i = 0; i < s.size; ++i)
	{
		cout << SLModify(s, i) << " ";
	}
	cout << endl;
	return 0;
}

七、内联函数

1、c语言中的宏

#include<stdio.h>
#define ADD(x,y) (x+y)
#define MUL(a,b) (a*b)

int main()
{
	printf("%d \n", ADD(2,3));

	int a = 2;
	int b = 3;

	printf("%d \n", MUL(a+b, a-b));

	printf("%d \n", ADD('a', 'asf'));

	return 0;
}

上面为c语言中一个宏的定义,我们可以看到宏就是很直接的替换,所以有些时候可能使用宏得到的结果并不是我们想要的,虽然在实现宏时我们应该多加一些括号,以避免这样的错误发生,但是我们写的宏总是会发生意想不到的错误,并且宏没有类型安全的检查,例如代码中我们将两个字符串使用宏相加也可以输出结果。而且宏是在c语言的预编译阶段直接替换的,所以我们也没有办法调试。所以可以总结出宏的优缺点。
优点
(1). 增强代码的复用性。
(2). 提高性能。
缺点
(1). 不方便调试宏(因为预编译阶段进行了替换)。
(2). 导致代码可读性差,可维护性差,容易误用。
(3). 没有类型安全的检查。
(4). 有些场景下非常复杂,容易出错,不容易掌握。
在这里插入图片描述
所以在c++中使用了内联函数来替代c语言中的宏技术。

2、内联函数

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。需要注意的是并不是被inline修饰的函数就一定是内联函数,该关键字只是建议编译器将该函数作为内联函数,如果该函数非常的复杂,编译器是不会将该函数作为内联函数的。
我们先看一下正常的函数是怎样实现调用的,下面是一个main函数调用Add函数的代码。
在这里插入图片描述
当我们来到对应的汇编文件中看调用Add函数的汇编代码是这样的。划线的汇编代码意思是将程序跳转到调用的Add子程序中,而该程序的地址就是后面的地址。
在这里插入图片描述
当我们跳转到这个地址后发现该地址是一个jmp跳转指令,跳转的地址为Add后面的地址。
在这里插入图片描述
当程序跳转来到最后的地址后我们发现该地址就是Add函数中第一条语句的地址,即该地址就是Add函数的地址。上面的一系列操作就是c语言中函数的调用,值得注意的是在刚开始执行call指令时,其实就要开始在栈中为这次函数调用开始建立栈帧了,即为该次函数调用分配空间。经过上面的分析我们可以总结出c语言的函数调用其实就是每个函数都有一个自己的地址,在这片内存空间中存储了该函数执行的具体指令,当需要调用该函数时,就会在栈中创建一个函数栈帧,然后根据地址跳转到该片内存空间开始执行这些指令。
在这里插入图片描述
我们再看一下内联函数在c++中是怎样实现调用的。下面为main函数调用内联函数Add。
在这里插入图片描述
可以看到此时在汇编文件中并没有像正常调用函数时那样的call指令,而是直接执行add指令。其实编译器在执行内联函数的调用时,并没有建立该函数的栈帧,而是将该函数的具体实现指令直接拷贝到了原来call的地方,这样就不需要建立函数栈帧了,而是接着执行下面的指令就可以了。这就是内联函数的效率为什么会比正常的函数高的原因,因为内联函数不需要建立栈帧。
在这里插入图片描述

3、内联函数特性

从上面的分析可以看出inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
例如swap函数有10行代码,如果不是内联函数的话,那么1000个调用swap的地方需要执行10+1000行代码;而如果swap是内联函数的话,那么每次调用swap函数的地方,都要将该函数的指令拷贝到这个地方,那么需要10*1000行代码,所以内联函数可能会使目标文件变大。所以编译器并不会无脑将所有用inline修饰的函数都变为内联函数,就是因为会将程序变得很大。所以我们在使用内联函数时,尽量将一些小型的函数设为内联函数。
在这里插入图片描述
并且inline内联函数不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。因为我们知道声明函数是告诉编译器这个函数的定义在后面,然后在链接阶段编译器就可以通过地址找到这个函数的指令,但是内联函数没有函数地址,所以就导致了在链接时找不到该函数的定义,就会报错。
在这里插入图片描述

八、auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

#include<map>
int main()
{
	int a = 0;
	//int b = a;
	auto b = a;  //auto会推断出来b的类型为int
	auto c = &a;   //auto会推断出来c的类型为指针

	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;

	map<string, string> dict;
	//map<string, string>::iterator dit = dict.begin();
	//上面的一大串类型可以直接使用auto来推导出来
	auto dit = dict.begin();
	return 0;
}

auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。


int main()
{
	int a = 0;
	auto b = &a;  //b为int*类型
	auto* c = &a;  //auto* 是说明c必须要为一个指针类型
	//auto* e = a;  //该语句错误,因为前面说明了e必须要为一个指针,而赋值时a不为指针
	auto& d = a;   //auto& 说明了d必须要为一个引用
}

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

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

auto不能推导的场景
auto不能作为函数的参数。
auto不能直接用来声明数组。

//auto不能作为函数的参数,因为编译器无法对a的实际类型进行推断
//void TestAuto(auto a)
//{
//
//}
int main()
{
	int a[] = { 1,2,3 };
	//auto不能直接用来声明数组
	//auto b[] = { 4,5,6 };
}

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

1、范围for的语法

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

int main()
{
	int arr[] = { 1,2,3,4,5 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; ++i)
	{
		arr[i] *= 2;
	}

	for (int* p = arr; p < arr + sz; ++p)
	{
		cout << *p << " ";
	}
	cout << endl;

	return 0;
}

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

int main()
{
	int arr[] = { 1,2,3,4,5 };
	//此时的e就代表arr中元素的引用,想改变数组的内容时就可以将e定义为引用
	for (auto& e : arr)
	{
		e *= 2;
	}
	//当不需要将数组中内容改变时,就可以不将e定义为引用
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

2、范围for的使用条件

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

void TestFor(int array[])
{
	//此时array作为参数传进来,不知道数组array的范围,所以无法使用范围for。
    for(auto& e : array)
        cout<< e <<endl;
}

十、指针空值nullptr(C++11)

1、C++98中的指针空值

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

int main()
{
	int* p1 = NULL;
	int* p2 = 0;

	return 0;
}

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

//如果没有定义NULL
#ifndef NULL
//如果在c++文件中
    #ifdef __cplusplus
    //就将NULL定义为整数0
        #define NULL 0
    //或者将NULL定义为(void*)0,即指针类型的0
    #else
        #define NULL ((void *)0)
    #endif
#endif

可以看到,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;
}

我们想通过f(NULL)调用参数为指针类型的f(int*)函数,但是由于在c++中NULL被定义为整数0,所以f(NULL)会调用f(int)函数,这就与我们的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
在这里插入图片描述
所以c++11中就新引入了nullptr关键字。
注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2.在C++11中,sizeof(nullptr) 与 sizeof((void *)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

在这里插入图片描述
在这里插入图片描述

  • 15
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值