C++基础讲解

发布于:2025-04-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

序言

由于C语言在语言表达、可维护性、和可扩展性等方面缺陷突出,所以在1983年本贾尼博士在C语言的基础上添加了面向对象编程的的特性,设计出了C++语言。由于C++是对C语言的发展和继承,所以C++中兼容C语言的绝大多数语法。但定义文件代码后缀需要改为 .cpp,Linux系统下不再用gcc编译,而是g++;vs编译器下会直接调用C++编译器。

1 命名空间

1.1 命名空间的作用

在C语言中,我们有时会发现,我们在新包含一个头文件后竟然出现了报错。比如我们自定义了一个rand变量,当我们包含了<stdlib>这个头文件后,出现了编译报错:C2365 “rand”: 重定义;以前的定义是函数。这是因为rand以前的定义是stdlib这个库中的函数,出现了命名冲突。所以针对这一问题C++中做了命名空间这一关键字。

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

int rand = 3;
int main()
{
	printf("%d\n", rand);
	return 0;
}

在这里插入图片描述

1.2,命名空间的定义

命名空间从本质上来讲是定义出了一个域,C++中有函数局部域、全局域、命名空间域和类域。命名空间域的定义需要用到关键字namespace,其定义语法为namespace _name{ },{}中的即为命名空间的成员。命名空间中可以定义变量、函数、类型等。namespace只能定义在全局,但它可以实现嵌套定义。命名空间域与全局域各自独立,不同的域中可以定义同名变量。且项目工程中多文件中定义的同名namespace会认为是一个namespace,它们会默认自动合并为一个命名空间域,不会冲突。
在这里要着重强调一下,C++的标准库都被封装在叫std(standard)的命名空间中。
代码演示:
1、定义一个命名空间:

//命名空间最后不要加;
namespace ZSY
{
//把自定义rand放到命名空间里就不会出现上面的命名冲突
	int rand = 8;

	struct List
	{
		struct List* next;
		int data;
	};

	int mod(int num1, int num2)
	{
		return num1 % num2;
	}
}

int main()
{
	int mod = 5;
	//打印的是局部变量mod的值
	printf("%d\n", mod);
	//打印的是命名空间中mod函数的指针
	printf("%p\n", ZSY::mod);
	return 0;
}

在这里插入图片描述

2、命名空间的嵌套定义:

namespace classmates
{
	namespace YHQ
	{
		int age = 88;
	}
	namespace WSB
	{
		int age = 89;
	}
}

int main()
{
	//打印classmates下YHQ下的age变量
	printf("YHQ_age == %d\n", classmates::YHQ::age);
	//打印classmates下WSB下的age变量
	printf("WSB_age == %d\n", classmates::WSB::age);
	return 0;
}

在这里插入图片描述

1.3 命名空间的使用

程序在编译阶段去查找一个变量的声明和定义时,默认只会在局部和全局查找且符合就近原则,不会到命名空间里去查找。所以当我们要使用命名空间里定义的变量、函数时,有三种方式:
1、指定命名空间访问(最推荐)
2、using将命名空间中某个成员展开(当经常访问不存在冲突的成员时最推荐)
3、展开命名空间中的全部成员(做项目时极不推荐,造成成员冲突的风险很大,但可以直接展开C++的std域,此外在日常练习时无关紧要)
代码演示:
演示前先介绍一种操作符:作用域解析运算符"::"
1、指定命名空间访问:

namespace Bye
{
	int b = 10;
}

int main()
{
	printf("b == %d\n", Bye::b);
	return 0;
}

2、using将命名空间中某个成员展开:

namespace data
{
	int a = 1;
	int b = 2;
}
//只展开命名空间里的 b
using data::b;
int main()
{
	printf("a == %d", data::a);
	printf("b == %d", b);
}

3、展开命名空间中的全部成员:

namespace data
{
	int a = 1;
	int b = 2;
}
//展开命名空间里的全部成员
using namespace data;
int main()
{
	printf("a == %d", a);
	printf("b == %d", b);
	return 0;
}

注:此处我们所提的展开命名空间的 “ 展开 ” 与展开头文件里的 “ 展开 ” 不是同一个意思。展开命名空间的展开好比打破了限制访问命名空间里成员的一堵墙;展开头文件则是对头文件内容的实行拷贝。

2 C++输入与输出

  1. <iostream>是C++标准的输入输出流库,其中定义了标准的输入与输出对象。
  2. <iostream>都被封装在命名空间std之中,在导入标准输入输出库后还应对std进行全展开或者对需要使用的部分进行展开。
  3. std::cinistream类的对象,它主要面向窄字符的标准输入流。
  4. std::coutostream类的对象,它主要面向窄字符的标准输出流。
  5. std::endl是一个函数,流插入输出时,等同于插入了一个换行字符同时刷新缓冲区。注意std::endl不可以完全等同于C语言当中的\n,\n虽然也能起到换行的作用,但是只有在部分条件下才能立即刷新缓冲区,与std::endl仍然存在差异。
  6. <<是流插入运算符(C语言中位运算的左移),>>是流提取运算符(C语言中位运算的右移)。
  7. C++中的输入输出与C语言相较更加简便,C++的输入输出可以自动识别变量类型,不需要像C语言的scanf/printf手动设置变量类型。更重要的是C++的流能更好的支持自定义类型对象的输入输出。(主要归功于运算符重载和面向对象编程)
  8. 在VS系列编译器中,包含<iostream>会间接包含<stdio.h>,但在其他编译器中不一定包含(如Linux下的g++)。
using namespace std;

int main()
{
	int ab = 10;
	double bc = 12.1;
	char cd = 'v';
	//流输出
	cout << ab << " " << bc << " " << cd << endl;
	std::cout << ab << " " << bc << " " << cd << std::endl;
	//手动设置输出格式
	printf("%d %f %c\n", ab , bc , cd);

	//流输入
	cin >> ab >> bc;
	//手动设置输入格式
	scanf_s(" %c", &cd);
	cout << ab << " " << bc << " " << cd << endl;

	return 0;
}

在这里插入图片描述
IO流需求较高处,可以加上以下3行代码,可以提高C++ IO效率。

int main()
{
    //提高C++ IO效率
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	return 0;
}

3 缺省参数

缺省参数是在声明或定义是给函数的参数设置一个缺省值。在调用该函数时,若没有相应实参传入,就采用该形参的缺省值;反之,则使用指定的实参。缺省参数分为全缺省半缺省。(缺省参数有时也称为默认参数)
缺省参数设置规则如下:

  1. 当函数的定义与声明分离时,缺省参数规定必须在函数声明处给缺省值,不能在函数定义与声明中同时出现。
  2. 全缺省就是全部参数给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次缺省,不能间隔给缺省值。
  3. C++规定带缺省参数的函数调用,必须从左往右依次给实参,不能跳跃给实参。

代码演示:
1、传参与不传参时缺省参数效果:

void Disp1(int a = 5)
{
	cout << a << endl;
}

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

在这里插入图片描述

2、全缺省与半缺省:

using namespace std;

//全缺省
void Disp1(int a = 10, int b = 20, int c = 30)
{
	cout << "a == " << a << endl;
	cout << "b == " << b << endl;
	cout << "c == " << c << endl;
	cout << " " << endl;
}

//半缺省
void Disp2(int c, int d = 40, int e = 50)//从右往左依次设置缺省值
{
	cout << "c == " << c << endl;
	cout << "d == " << d << endl;
	cout << "e == " << e << endl;
	cout << " " << endl;
}

int main()
{
    //从左往右依次指定实参
	Disp1();
	Disp1(100);
	Disp1(100, 200);
	Disp1(100, 200, 300);

	Disp2(100);
	Disp2(100, 200);
	return 0;
}

在这里插入图片描述

4 函数重载

C++中支持同一作用域中出现同名函数,但要求这些同名函数的形参不同而不是返回值类型不同,可以是参数个数不同或者参数类型不同。如此C++函数的调用就表现出了多态行为,更为灵活。而C语言是不支持同一作用域中出现同名函数的。
1、形参类型不同:

//参数类型不同
using namespace std;
//参数类型为整型
int Multiplication(int a = 10, int b = 5)
{
	return a * b;
}
//参数类型为浮点型
float Multiplication(float a = 10.0 , float b = 1.1)
{
	return a * b;
}

int main()
{
    //调用函数,实现函数重载
	int x = Multiplication(12,13);
	float y = Multiplication(11.0f, 12.1f);
	
	cout << "int Multiplication(int a = 10, int b = 5)" << endl;
	cout << x << endl;
	cout << "float Multiplication(int a = 10, int b = 5)" << endl;
	cout << y << endl;
	return 0;
}

在这里插入图片描述
2、形参个数不同:

//形参个数不同
using namespace std;
//一个形参
int division(int a)
{
	return a;
}
//两个形参
double division(double c , double d)
{
	return c - d;
}

int main()
{
	//调用函数,实现函数重载
	int x = division(8);
	double y = division(12.2, 0.2);

	cout << "int division(int a)" << endl;
	cout << x << endl;
	cout << "double division(double c , double d)" << endl;
	cout << y << endl;
	return 0;
}

在这里插入图片描述
3、参数类型顺序不同:

//参数类型顺序不同
using namespace std;

void inf(char* x , int a)
{
	cout << "姓名:" << x << " " << "年龄:" << a << endl;
}

void inf(int a , char* x)
{
	cout << "体重(kg):" << a << " " << "营养状况:" << x << endl;
}

int main()
{
	char name[5] = "";
	int age;
	int weight;
	char situation[5] = "";
	cin >> name >> age >> weight >> situation;
	inf(name, age);
	inf(weight, situation);

	return 0;
}

在这里插入图片描述

4、避免如下歧义编写:
下面这两个函数构成函数重载,但c1()在调用时会报错,存在歧义,编译器不知道调用谁。

//避免下面存在歧义的编写
using namespace std;
void c1()
{
	cout << "hello" << endl;
}
void c1(int a = 10)
{
	cout << a << endl;
}

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

在这里插入图片描述

5 引用

5.1 引用的概念与特性

引用可以理解为给现有的变量取一个别名,而非定义一个新的变量。编译器不会给这个引用变量开辟内存空间,引用变量是和它所引用的对象共用同一块内存空间(注意:这里所说的对象是笼统意义上的对象,包括了类实例化的对象、临时副本和内置类型对应数据。下文若无特殊说明,对象均为此含义)。对于引用的理解,我们可以类比不同人对诸葛亮的称呼,比如诸葛亮本名就是诸葛亮,刘备叫他军师、丞相后者卧龙先生,徐庶叫他孔明,这些别人对他的称呼就等同于我们所提的引用。
引用的语法格式:类型& 引用别名 = 引用对象。注意,在C++中为了避免引入过多的运算符,复用了一些C语言的运算符,比如前文所提的流插入和流提取运算符(<<和>>),&也是一样,在C++中既能表示取地址运算,也可以用于引用。
引用的特性有以下三点:

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

注意:这只是在C++中引用的特性,在其他语言中会有差异,比如在JAVA中引用可以改变其指向,而在C++中却不行。
在这里插入图片描述

using namespace std;

int main()
{
	int x = 10;
	//初始化引用变量
	int& y = x;

	int z = 0;
	//想改变引用变量y的指向,但是C++中不允许这么操作,所以下面的命令就是赋值命令
	y = z;
	//因为y是x的别名,修改了y也等同于修改了x,所以x、y、z都是0
	cout << x << endl;
	cout << y << endl;
	cout << z << endl;
	//x与y地址相同,二者与z的存储地址不同
	cout << &x << endl;
	cout << &y << endl;
	cout << &z << endl;
}

在这里插入图片描述

5.2 引用的使用

  1. 引用主要用于(1)引用传参和引用做返回值中减少拷贝提升效率 (2)改变引用变量时同时改变被引用的实体。 因为引用传递是直接通过别名访问原始对象,所以无需拷贝,也可直接改变原始对象。
  2. 引用传参与指针传参功能是类似的,引用传参相对更方便一些。
  3. 引用和指针在实践中是相辅相成的,二者的功能虽然近似,但是仍有不同,二者间不存在替代关系。
    以下代码分别展示引用传参和引用做返回值的情况:

5.2.1 引用传参

void change(int& a, int& b)//x与y传上来后,a与b就是它俩的别名,改变a、b就是改变x、y
{
	a++;
	b++;
}

int main()
{
	int x = 10 , y = 20;
	cout << "初始x的值:" << x << " \n" << "初始y的值:" << y << endl;

	change(x, y);
	cout << "改变后x的值:" << x << " \n" << "改变后y的值:" << y << endl;
	return 0;
}

在这里插入图片描述
上面这段代码就是形参处用引用来接受传参,如此一来形参的改变就可以直接影响实参,而不需要使用指针来接受传参,且避免了因传值传参需要拷贝而额外在栈区开辟空间,提升了运行效率。

5.2.2 引用做返回值

5.2.2.1采用引用返回:
int& getnum()
{
	static int c = 10;
	return c;
}

int  main()
{
	int x = 50;
	cout << "初始c值:" << getnum() << endl;
	//此处返回的是c的别名,下面就是把x的值赋值给c的别名
	getnum() = x;
	cout << "改变后c值:" << getnum() << endl;
	return 0;
}

在这里插入图片描述

5.2.2.2采用值返回的情形:
int getnum()
{
	static int c = 10;
	return c;
}

int  main()
{
	int x = 50;
	cout << "初始c值:" << getnum() << endl;
	//此处返回的不是引用,而是值。左值是不能被改变的,所以报错
	getnum() = x;
	cout << "改变后c值:" << getnum() << endl;
	return 0;
}

在这里插入图片描述
在上面的代码中,函数采用值返回的格式,当函数调用后会为返回的对象创建一个临时对象(临时副本),返回值返回的是这个临时对象而非原始对象,C++规定临时对象具有常性且临时对象和原始对象是各自独立存在的,所以修改临时对象内的内容是不会影响到原始对象的。
此外,函数采用值返回时,函数的返回值是右值,右值通常只能赋值给其他实体而不能被其他实体赋值。所以上面代码中getnum() = x;的报错表示为左侧函数的返回值应当是可以被修改的左值,即应当返回引用。

特殊说明:
内置类型成员在进行传参,按值返回或类型转换等情况时编译器会把数值直接复制到寄存器或栈,寄存器或栈上的数据就是我们所说的临时副本即笼统意义上的对象。
自定义类型成员在进行传参,按值返回等情况时编译器会创建临时对象(这个对象是类和对象概念中的对象含义),通常创建在栈区,拷贝行为会调用拷贝构造函数(类和对象章节会进行讲解)。
在这里插入图片描述

5.2.2.3 函数返回值与左值右值的关联

由上可知,函数采用按值返回时,其返回值是右值;采用按引用返回时,其返回值是左值。从之前链表章节我们可知,函数的返回值是指针时,我们依然可以对这个指针指向的内容进行改变,那么采用指针返回时,返回值是左值吗?
对于这个问题我们要知道当指针作为返回值返回到调用处后,其返回的是一个地址值,这个地址值指向某个内存位置,尽管指针本身在函数返回时是一个右值(因为它是一个临时的、不可修改的地址值),但是这个地址值所指向的内存位置可能是一个可修改的变量(因为这块内存位置可能存储的的具有常性的数据)。

//返回值是指针
int* getnum()
{
	static int c = 10;
	return &c;
}

int  main()
{
	int x = 50;
	cout << "初始c值:" << *getnum() << endl;
	//这里返回的是一个地址值,解引用后其所指向的内存位置是可以修改的
	* getnum() = x;
	cout << "改变后c值:" << * getnum() << endl;
	return 0;
}

在这里插入图片描述

5.3 const引用

  1. const引用既可以引用一个const对象,也可以引用一个普通对象,因为对象的访问权限在引用的过程中可以缩小,但不能放大。(普通引用来引用const对象就是权限放大,由可读不可写放大为可读可写,这是不允许的;const引用来引用普通对象就是权限缩小,由可读可写缩小为可读不可写,这是允许的)
  2. **在C++中若对生成的临时对象进行引用,也需要使用const引用。**在C++中临时对象指的是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,且C++规定这类临时对象具有常性。

常见的生成临时对象的情景有:
1、±等产生的运算结果;2、类型转换;3、传参;4、返回值。
在这里插入图片描述
在这里插入图片描述
在上图中,左侧是给a+b生成的临时对象定义了一个const引用;右侧看似pa是a的引用,实则pa引用的是隐式类型转换时生成的临时对象,所以a的类型不变仍是double,pa则是将生成的临时对象的值截断为int类型。

5.4 指针和引用关系

C++中指针和引用是相辅相成的,各有特点,相互之间有重叠性但又相互不可替代。
3. 语法概念上引用是一个变量的别名,不开空间。指针是存储一个变量的地址,要开空间。
4. 引用定义时必须初始化,而指针是建议初始化,其在语法上未做要求。
5. 在C++中,引用一旦初始化后就不可改变指向,而指针是可以不断改变指向对象。
6. 引用可以直接访问指向对象,而指针需要解引用才能访问指向对象。
7. 二者在关键字sizeof中含义不同,引用的结果是其引用类型的大小,但指针的大小始终是地址空间所占的字节个数(x86下是4字节,x64下是8字节)
8. 指针若不及时置空,极易出现空指针或野指针的问题。而引用却很少出现,相对而言使用引用更安全一些。(比如函数采用引用返回,但是返回值却是函数内的局部变量,生命周期结束函数销毁,此时引用就为空)

6 inline修饰的内联函数

  1. inline修饰的函数称为内联函数,C++编译器编译时会在调用处展开内联函数,这样函数就不需要建立栈帧,可以提效。
  2. inline对于C++编译器来说只是一个建议,有时即使你对函数使用inline修饰,编译器也可以选择在调用的地方不展开,这取决于编译器的类型与版本,不同编译器针对inline有不同的结果,因为C++中并未对此作出规定。
  3. inline适用于频繁调用的短小函数,对于递归函数或代码较多的函数,即使加上inline也会被编译器忽略。
  4. C++中设计inline的目的主要是替换C语言中的宏函数。虽然宏函数也会在预处理时展开,但是宏函数实现相对复杂易出错,不便于调试。故使用inline修饰的内联函数可最大程度规避这些问题。
  5. inline不建议定义与声明分离到两个文件,分离会导致链接错误。学习完C语言后我们知道,常规函数调用如果函数的声明和定义分离时,函数在完成定义的同时会生成一个外部符号(接口),函数的声明就是告知编译器函数的接口(返回类型、函数名、参数类型),链接时通过该接口找到所调用函数的地址。而定义inline修饰的内联函数时,编译器不会为其生成外部符号,所以当声明和定义分离到两个文件时,链接器无法找到外部符号从而无法找到要调用函数的地址,导致链接错误。正确的做法应当是将函数的声明和定义放到同一头文件中。
  6. VS编译器的Debug版本下为了方便调试,默认是不展开inline的。
//内联函数声明
inline void swap(int* a, int* b);

//内联函数定义
void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

int main()
{
	int x = 1;
	int y = 2;
	swap(&x, &y);
	cout << x << endl;
	cout << y << endl;
	return 0;
}

7 空指针nullptr

在C语言中空指针NULL是一个宏,其被定义为无类型指针(void*)的常量。但在C++的头文件中,对NULL的宏定义是这样的:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0            // C++中定义为0(整型)
#else
#define NULL ((void*)0)   // C中定义为void*指针
#endif
#endif

所以在C++中,NULL的定义不会是void*类型,而是被定义为整型字面量0。这是因为C语言中void*允许进行隐式指针转换而C++中是不允许的。
由此拓展一下为什么C++中不能将NULL定义为void*
原因一:C++的类型安全要求:
C++不允许将void*隐式转换为其他指针类型(如int*char*)。如果NULL被定义为void*,则以下代码会导致编译错误:

int* p = NULL;  // 如果NULL是void*,C++需要显式类型转换

因此,C++将NULL定义为整型0,可以隐式转换为任何指针类型(如int*char*),从而兼容旧代码。
原因二:函数重载的歧义问题:

//函数1
void f1(int a = 10)
{
	cout << a << endl;
}
//函数2
void f1(char* x)
{
	cout << x << endl;
}

int main()
{
	f1(NULL);// NULL是0,调用函数1
	return 0;
}

在这里插入图片描述

上面代码中想通过传空指针来调用函数f1(char* )时,由于空指针NULL被定义为了0,会调用f(int ),与程序初衷相悖。

所以为了解决上面的问题,C++11中引入了关键字nullptr,它是一种特殊类型的字面量(空指针常量),它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式转换为指针类型,而不能被转换为整型。

int* p = nullptr;    // 正确,无需类型转换
func(nullptr);        // 明确调用指针版本的函数

全文至此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢(▽)(▽),这将是对我最大的肯定与支持!!!谢谢!!!(▽)(▽)