从本篇文章开始,我们正式进行C++的系统学习。C++是在C语言的基础上添加了面向对象编程的特性,是C语言的延伸,并遵循C语言的绝大多数语法。如果想学习C++,必须要有一定的C语言基础,这样学起来才不会太过痛苦。
本文章即假设读者已经有过学习C语言的经历,对于C++中所包含的C语言知识,在原则上不再进行解释,只有在过于易错或遗忘的点上进行提醒。
摘要:本文主要对C++的基础部分进行讲解,包括命名空间namespace,C++的输入与输出,缺省参数,函数重载,引用及其特性,const修饰引用,inline修饰函数,以及NULL在C与C++中的不同意义与nullptr。
目录
[1.1 C++的诞生与发展](#1.1 C++的诞生与发展)
[1.2 C++在工作中的应用领域](#1.2 C++在工作中的应用领域)
[二、namespace 命名空间](#二、namespace 命名空间)
[2.1 namespace的意义](#2.1 namespace的意义)
[2.2 namespace的使用规则与域作用限定符 "::"](#2.2 namespace的使用规则与域作用限定符 “::”)
[3.1 关于](#3.1 关于)
[3.2 std::cin与std::cout](#3.2 std::cin与std::cout)
[3.3 关于竞赛](#3.3 关于竞赛)
[4.1 缺省参数的解释](#4.1 缺省参数的解释)
[4.2 全缺省参数与半缺省参数](#4.2 全缺省参数与半缺省参数)
[5.1 什么是函数重载?](#5.1 什么是函数重载?)
[5.2 函数重载示例:](#5.2 函数重载示例:)
[5.3 特殊情况](#5.3 特殊情况)
[6.1 引用的概念和定义](#6.1 引用的概念和定义)
[6.2 引用与指针的区别](#6.2 引用与指针的区别)
[6.3 const修饰引用](#6.3 const修饰引用)
[7.1 什么是inline?](#7.1 什么是inline?)
[7.2 使用inline的示例](#7.2 使用inline的示例)
一、引言部分
1.1 C++的诞生与发展
在二十世纪七十年代中期,Bjarne Stroustrup(本贾尼 斯特劳斯特卢普)在剑桥大学计算机中心工作,他希望开发一个既要编程简单、正确可靠,又要运行高效、可移植性强的计算机程序设计语言。而以C语言为背景,Simula思想为基础的语言,正好符合Bjarne Stroustrup的预期。
于是在1979年,Bjarne Stroustrup在AT&T贝尔实验室开始从事将C改良为带类C(C with classes)的工作,并在1983年将该改良的C语言正式命名为C++。
C++之父 Bjarne Stroustrup
1989年,C++的标准化工作正式展开, 成立了ANSI与ISO(International Standards Organization,国际标准化组织)的联合标准化委员会。
1994年,联合标准化委员会提出了第一个标准化草案,而在第一个标准化草案完成后不久,联合标准委员会通过了将STL(Standard Template Library,惠普实验室旗下开发的一系列软件的统称)包含到C++标准中的提议。
1997年,联合标准化委员会通过了该标准的最终草案。
1998年,C++的ANSI/ISO标准被投入使用,是C++官方的第一个版本,被称为C++98。
此后,C++版本五年一次更新,2011年后,C++版本三年一更。目前,C++23已更新完成,
C++26正在制定中...
对于不同的编译器,支持的C++版本可能不同,应留心编译器所支持的C++版本,避免造成编写上的错误。
1.2 C++在工作中的应用领域
我对C++也不十分了解,以下是文心一言的回答:
|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 系统软件开发 | * 操作系统开发:C++因其高效的性能和灵活的内存管理机制,非常适合用于开发操作系统。例如,Windows的部分组件和Linux的某些部分都使用了C++进行开发。 * 编译器开发:C++编译器自身也是用C++编写的,体现了C++在编译器开发中的强大能力。 * 数据库管理系统:C++可以通过ODBC、JDBC等接口访问各种类型的数据库,开发高性能、可扩展的数据库应用程序。 |
| 嵌入式系统开发 | * 嵌入式系统需要高效的执行速度和底层控制能力,C++的高性能和指针机制非常适合用于开发这类系统。例如,智能家居设备、汽车电子系统、工业控制系统等嵌入式系统都广泛采用C++进行开发。 |
| 游戏开发 | * 游戏引擎开发:C++拥有高效的性能和强大的图形库支持,是游戏开发的主流语言之一。许多著名的游戏引擎,如Unity3D和Unreal Engine,都是使用C++编写的。 * 游戏逻辑实现:C++在游戏开发中不仅用于游戏引擎的开发,还用于实现游戏的核心逻辑,确保游戏的流畅运行和丰富的交互体验。 |
| 高性能计算 | * C++的高性能和底层控制能力使其成为高性能计算领域的理想选择。在科学计算、金融建模、人工智能等领域,C++都发挥着重要作用。例如,深度学习框架TensorFlow就使用了C++进行底层开发。 |
| 网络编程 | * C++拥有许多成熟的用于网络通信的库,如ACE库等,这些库使得C++在网络编程中具有强大的竞争力。C++可以用于开发网络应用程序,如服务器端开发、网络协议实现等。 |
| 桌面应用开发 | * C++可以利用其丰富的GUI库和底层库,开发高性能、跨平台的桌面应用程序。例如,使用Qt或MFC等框架开发的桌面应用程序,通常都具有出色的性能和用户体验。 |
| 音视频处理 | * 音视频处理需要高效的算法和数据处理能力,C++作为一种高性能的编程语言,在音视频采集、处理、编码和解码等方面都有广泛应用。例如,音频编辑软件Audacity和视频编辑软件Adobe Premiere Pro都使用了C++进行开发。 |
| 虚拟现实与数字图像处理 | * 虚拟现实和数字图像处理领域也在不断发展,C++凭借其强大的计算能力和灵活性,在这些领域中也有广泛应用。例如,基于C++开发的虚拟现实系统和数字图像处理软件能够提供更真实、更高效的用户体验。 |
| 分布式应用与移动开发 | * 尽管C++在分布式应用和移动开发中的应用不如其他语言(如Java或Swift)普遍,但凭借其对C的兼容性和面向对象性质,C++也开始在这些领域崭露头角。特别是在需要高性能和底层控制的分布式应用或移动应用中,C++仍然具有一定的优势。 |
在阅读完上述表格后,相信读者已经对C++的工作应用领域有所了解。
既然我们已经大概了解了C++的历史与发展趋势,接下来,我们正式进行C++的系统学习。
注:C++文件的后缀名是.cpp
二、namespace 命名空间
2.1 namespace的意义
在C语言中,我们经常会对变量、函数、结构体等的取名犯难,一旦在一个域中出现相同名称,编译器就会报错,这是十分令人苦恼的一件事,而C++则想出了解决办法,便是使用命名空间。让我们看下面这两个代码:
代码2.1.1:
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
在VS下运行上述代码,会发生运行警告:
原因便在于,rand在头文件<stdlib.h>中已经被定义为函数,再次在全局中定义rand,会在使用rand时发生冲突,如果把rand在main函数里进行定义,就不会发生报错(局部域的优先级高于全局域)。
那如果我不想在main中定义rand,该如何解决呢?
代码2.1.2:
cpp
#include <stdio.h>
#include <stdlib.h>
namespace LLD
{
int rand = 10;
}
int main()
{
printf("%d\n", LLD::rand);
return 0;
}
程序运行结果:
符合我们的预期。
接下来,我们看看命名空间到底怎么使用。
2.2 namespace的使用规则与域作用限定符 "::"
namespcae本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量。它不影响变量的生命周期。
cpp
namespace space_name//命名空间的框架
{
//...
}
命名空间的域中可以有变量、函数、结构体定义...
例:
如果想要使用namespace的域里的内容,需要用 namespace的空间名+"::"+变量
代码 2.2.1:
cpp
#include <stdio.h>
namespace LLD1
{
int a = 1;
char b = 2;
int Add(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int data;
};
}
int main()
{
struct LLD1::Node s1 = { nullptr,1 };
printf("%d\n", LLD1::a);
printf("%d\n", LLD1::Add(2, 3));
printf("%d\n", s1.data);
return 0;
}
程序运行结果:
值得注意的是,对于结构体,前缀是在struct之后添加的。
如果不添加变量之前的XX+"::"作为前缀,编译器就会报错。(也可以不添加,往下看)
如果我们在上述代码上添上一行"using namespace LLD1;",就可以不添加前缀。
代码2.2.2:
cpp
#include <stdio.h>
namespace LLD1
{
int a = 1;
char b = 2;
int Add(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int data;
};
}
using namespace LLD1;
int main()
{
struct Node s1 = { nullptr,1 };
printf("%d\n", a);
printf("%d\n", Add(2, 3));
printf("%d\n", s1.data);
return 0;
}
运行结果与代码2.2.1一致。
但是如果我们加上了 "using namespace LLD1;",就不能再在全局域中命名与LLD1内的同名的变量了。
除非使用率高,且在全局域中不存在与命名空间内变量名字相同的全局变量,否则不建议使用该语句。
全局域中可以存在多个命名空间,命名空间只能定义在全局,但命名空间里可以继续嵌套命名空间。
代码2.2.3:
cpp
#include <stdio.h>
int a = 1;
namespace LLD1
{
int a = 2;
namespace LLD1
{
int a = 3;
}
}
namespace LLD2
{
int a = 4;
}
int main()
{
printf("%d\n", a);
printf("%d\n", LLD1::a);
printf("%d\n", LLD1::LLD1::a);
printf("%d\n", LLD2::a);
return 0;
}
程序运行结果:
是不是很奇妙,很有意思?
同时,项目中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
三、C++的输入与输出
C++的第一个程序:
代码3.0:
cpp
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!" << endl;
return 0;
}
3.1 关于<iostream>
<iostream>是Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
只要我们引用了<iostream>,就可以在工程中使用std::cin、std::cout等对象,std::endl等函数,同时,<iostream>中引用了头文件<stdio.h>(假设编译器是VS),如果想使用scanf、printf等函数,不需要再对<stdio.h>进行引用。
3.2 std::cin与std::cout
std::cin是istream类的对象,它主要面向窄字符的标准输入流**。**
std::cout是ostream类的对象,他主要面向窄字符的标准输出流**。**
<<是流插入运算符,与std::cout配合使用;>>是流提取运算符,与std::cin配合使用;
(在C语言中,<<是左移运算符,>>是右移运算符)
简单且通俗来说,std::cin与C语言里的scanf相似,std::cout与C语言里的printf相似。但是它们又有不同,printf与scanf需要告知其打印或接收的数据的类型,而std::cout与std::cin则不需要,而且std::cin不需要额外提供变量的地址。
关于std::endl,它是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区,与"\n"的功能相同,通俗的说就是换行。但是在C++中,比起"\n",更建议使用std::endl,因为仅Windows支持\n,别的系统例如Linux不一定支持,使用std::endl的话,代码的可移植性更高。
同时,因为会经常使用std::cin与std::cout以及std::endl,建议在最前面加上"using namespace std;"这一行代码,这样前缀std::就可以省略了。
注:本文之后都将std::cin、std::cout、std::endl分别简写为cin、cout、endl;
请观察以下代码与程序运行结果,比较并理解cin与scanf,cout与printf,"\n"与endl的区别。
代码3.2.1
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cout << "请分别输入a、b、c的值:";
cin >> a >> b >> c;
printf("%d %lf %c\n",a,b,c);
printf("请再次分别输入a、b、c的值:");
scanf("%d %lf %c", &a, &b, &c);
cout << a << " " << b << " " << c << endl;
return 0;
}
程序运行结果:
3.3 关于竞赛
在io需求比较高的地方,如部分大量输入的竞赛题中,在程序中加上以下三行代码,可以提高程序效率。
cpp
int main()
{
//在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下三行代码
//可以提高C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
四、缺省参数
4.1 缺省参数的解释
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。调用该函数时,如果没有指定实参,则采用形参的缺省值,否则使用指定的实参。
代码4.1.1
cpp
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << "a = " << a << endl;
}
int main()
{
Func();
Func(9);
return 0;
}
程序运行结果:
4.2 全缺省参数与半缺省参数
缺省参数分为全缺省参数 和半缺省参数 ,全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。
但是,C++规定,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
例如:
cpp
//...
int Fun1(int a,int b=3,int c=4)//正确的半缺省参数
{
return a+b+c;
}
int Fun2(int a=3,int b=4,int c)//错误的半缺省参数
{
return a+b+c;
}
int Fun3(int a=3,int b,int c=4)//错误的半缺省参数
{
return a+b+c;
}
int main()
{
//...
Fun1(4);
Fun1(1,2,3);
//...
}
带缺省参数的函数调用时,C++规定必须从左到右依次给实参,不能跳跃给实参。
例如:
cpp
//...
int fun1(int a=1,int b=2,int c=3)
{
return a+b+c;
}
int fun2(int a,int b=2,int c=3)
{
return a+b+c;
}
int main()
{
//...
fun1(1,2);//正确的调用
fun1(1);//正确的调用
fun1(,2,);//错误的调用
fun1(,,3);//错误的调用
fun2(1);//正确的调用
fun2(1,2);//正确的调用
fun2(,2,);//错误的调用
fun2(,2,3);//错误的调用
//...
}
注:函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
例:
运行报错:
报错在于参数a和b被重定义。
如果将main.cpp中a的赋值与b的赋值删去,则程序运行正常,且结果为:
五、函数重载
5.1 什么是函数重载?
在C语言中,程序是不允许有名字相同的两个函数出现的,即使它们的形参个数不同,类型不同。
而在C++中,为了解决C语言对于函数名的限制,给出了函数重载这一概念。
所以到底什么是函数重载呢?
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是形参类型不同,也可以是形参个数不同,或者二者兼而有之,这样C++函数调用就表现出了多态行为,使用更灵活,而我们把那些同名函数,称为构成函数重载。
5.2 函数重载示例:
代码5.2.1:
cpp
#include <iostream>
using namespace std;
//参数类型不同
int Add(int x, int y)
{
cout << "int Add(int x,int y);" << endl;
return x + y;
}
double Add(double x, double y)
{
cout << "double Add(double x,double y);" << endl;
return x + y;
}
//参数个数不同
void fun()
{
cout << "void fun()" << endl;
}
void fun(int a)
{
cout << "void fun(int a)" << endl;
}
//参数类型顺序不同
void Fun(int a, char b)
{
cout << "void Fun(int a, char b)" << endl;
}
void Fun(char b,int a)
{
cout << "void Fun(char b,int a)" << endl;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
fun();
fun(2);
Fun(1, 'b');
Fun('b', 1);
return 0;
}
程序运行结果:
根据代码5.2.1的运行结果,我们可以大概知晓什么是函数重载、函数重载的类型及其使用了。
值得注意的是,如果仅函数的返回值的类型和值不同,而函数参数完全相同,是无法构成函数重载的。
5.3 特殊情况
在某些情况下,即使构成函数重载,编译器依然会报错。
如图:
原因便在于"f1();"这一句代码存在歧义,编译器无法确定是调用第一个函数f1()还是调用第二个有全缺省参数的f1(),因而报错。
所以构造重载函数时,应当思考是否会造成如上的情况。避免出现编译上的错误。
六、引用
6.1 引用的概念和定义
引用不是定义一个新的变量,而是给已存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如有人说卧龙在吃面,我们就知道是诸葛亮在吃面,有人说凤雏和卧龙打起来了,我们就知道庞统和诸葛亮打起来了。
引用的基本语法是:
类型& 引用别名 = 引用对象;
注意:
1.引用的类型必须与引用对象的类型一致。
2.引用的空间大小取决于引用对象的空间大小,与引用对象一致。
3.可以有多个引用作为同一个引用对象的别名,但一个引用只能引用一个引用对象
4.定义引用时必须初始化,且引用去引用一个引用对象后就不可再改变使其成为另一个引用对象的引用。
代码6.1.1:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = a;
int& d = c;
++a;
cout << a << endl;
++b;
cout << a << endl;
++d;
cout << a << endl;
++c;
cout << a << " " << b << " " << c << " " << d << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
程序运行结果:
根据代码6.1.1的运行结果可以看到,a,b,c,d的地址是一致的,b,c,d都是变量a。其中d是c的别名,而c是a的别名,故d是a的别名。
6.2 引用与指针的区别
在C++中,指针与引用像是两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但各有各自的特点,互相不可替代。
让我们通过表格来看看他们二兄弟的主要区别:
|-----|--------------------------------------------------------------------|
| 区别一 | 在语法上,引用是一个变量取别名,不开辟空间,而指针是储存一个变量地址,需要开辟空间。 |
| 区别二 | 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。 |
| 区别三 | 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象。 |
| 区别四 | sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占四个字节,64位平台下占八个字节) |
| 区别五 | 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。 |
代码6.2.1:
cpp
#include <iostream>
using namespace std;
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1, b = 2;
int c = 3, d = 4;
Swap(&a, &b);
cout << a << " " << b << endl;
Swap(c, d);
cout << c << " " << d << endl;
return 0;
}
程序运行结果:
从代码6.2.1及其运行结果中我们不难看出,引用在函数访问外域变量并改变其值方面比指针更方便,指针需要函数接收地址,解引用对其进行访问,而引用则只需要接收变量名,即可在函数内对其进行访问。
引用在函数中的另一个优势是,可以作为返回值类型返回变量。
代码6.2.2
cpp
#include <iostream>
using namespace std;
int& fun(int& a)
{
cout << "int& fun(int& a)" << endl;
return a;
}
int main()
{
int b = 3;
fun(b) = b + 1;
cout << b << endl;
return 0;
}
程序运行结果:
这一特性在后续C++的学习与使用中还是很有用处的。
引用有没有危险使用的时候?
让我们看下图代码:
运行虽然没有问题,但是变量a在函数结束后就被销毁了,这时再对函数返回的变量进行访问修改就属于非法访问,会有危险。此种类型情况一定要避免 !!!
6.3 const修饰引用
引用同样可以被const修饰,且如果引用对象被const修饰,也会影响引用。接下来让我们进行详细的讲解。
(1)如果引用变量被const修饰,引用也需被const修饰,否则会报错。
正确的做法:
在C++中,如果引用变量被const修饰,而引用不被const修饰,就会造成权力的放大,即可以通过引用改变被const修饰的引用变量的值,这在C++中是不被允许的。 故而如果引用变量被const修饰,引用也需被cosnt修饰,否则编译器会进行报错。
(2)如果引用变量未被const修饰,而引用被const修饰,则引用无法改变值,而引用变量可以改变自己的值,这是权力的缩小。
代码6.3.1:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 233;
const int& b = a;
a++;
cout << b << endl;
return 0;
}
程序运行结果:
被const修饰的引用可以用来接收常值,例如:
再例如:
在以上两个场景中,a*3的结果保存在一个临时对象中,const int& i=b;也是如此,b的强制类型转换值的结果保存在一个临时对象中,引用b与引用i都是引用的临时对象,而C++规定临时对象具有常性,所以需要用cosnt修饰引用。
PS:所谓临时对象就是编译器在需要一个空间暂存表达式的求值结果时所临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
这一特性在以后中经常用于做函数的形参类型,例如:
七、inline
7.1 什么是inline?
inline可以修饰函数,被inline修饰的函数叫做内敛函数,编译时C++编译器会在调用的地方展开内敛函数 ,这样调用内敛函数就不需要建立函数栈帧了,可以提高程序的运行效率。
注意:inline适用于频繁调用的短小函数,对于递归函数,代码相对多的函数,即使被inline修饰,编译器也会忽略,在函数调用时建立函数栈帧。
inline的设计初衷是为了代替用宏写的函数,而用宏的目的也是为了替代简单函数,使程序的运行效率更高。
在代码的底层,汇编代码上,被inline修饰的函数仅仅是隐藏了函数栈帧的创建过程,故而如果大型函数使用inline,就会造成在代码转化为可执行文件exe时使其文件内存激增。因而很多编译器对inline在什么情况下展开都有自己的标准。
7.2 使用inline的示例
代码7.2.1:
cpp
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
return ret;
}
int main()
{
//可以通过汇编观察程序是否展开
//有call Add语句就是没有展开,否则是展开
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
需要注意的是,VS编译器 debug版本默认不展开inline,这样方便调试,想要展开需要自行进行设置。
这里不再对inline进行深入讨论了,浅尝辄止即可,在以后的学习中有机会再对inline进行深度说明。
只需要记住,inline修饰的函数是为了在调用该函数时不创建函数栈帧,以提高程序的运行效率,且inline只对相对简单的函数有作用,过于复杂的函数即使被inline修饰,也会被编译器忽略。
重要的一点:内敛函数的声明与定义不能像普通函数一样分开,必须在同一个文件中!
八、NULL与nullptr
NULL实际上是一个宏,在传统的C头文件stddef.h,有关NULL的内容如下:
C++中,NULL被定义为字面常量0,而在C中则被定义为(void*)类型的常量。
故而在C++中,不能像在C中一样的去使用NULL,在C++11中引入了nullptr,nullptr是一个特殊的关键字,一种特殊类型的字面量,它可以转换成其他任意类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
C++中的nullptr可以说是C中的NULL的升级版。
本文完!
第一次突破1W字,纪念一下。