前言:
本篇博客将带大家认识C++,熟悉基本语法
认识C++
C++的诞生与发展
C++ 是由丹麦计算机科学家贝尔纳斯·斯特劳斯特博士(Bjarne Stroustrup)于20世纪80年代初期开发的。他在贝尔实验室工作时,对 C 语言进行了扩展,加入了OOP(object oriented programming:面向对象)思想,从而诞生了 C++。C++ 的名字中的 “++” 表示在 C 语言的基础上的一个增量,暗示这是对 C 的增强和扩展。因此:它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++ 在其发展过程中经历了多个标准版本的发布,其中最早的标准是于1998年发布的 C++98,之后又有 C++03、C++11、C++14、C++17 和 C++20 等版本。每个版本都引入了新的特性和改进,使得 C++ 成为一个强大而灵活的编程语言。
" C with Classes" 是 C++ 的前身,这个术语最初用于描述贝尔纳斯·斯特劳斯特博士在开发 C++ 时的初期版本。C++ 最初是在 C 语言的基础上引入了一些面向对象编程的概念,而 “C with Classes” 就是这个过渡阶段的命名。
C++ 在行业中的运用
-
系统级编程: C++ 被广泛用于开发操作系统、驱动程序和其他系统级软件,因为它提供了对硬件的底层访问和高效的系统编程能力。
-
游戏开发: 许多游戏引擎和大型游戏项目都使用 C++。其高性能和底层控制使其成为游戏行业的首选语言之一。
-
嵌入式系统: C++ 被用于开发嵌入式系统,如智能家居设备、汽车控制系统等。它的效率和灵活性使其适用于资源受限的环境。
-
金融领域: 在金融行业,C++ 被广泛用于开发高性能的交易系统、风险管理工具和其他金融应用。
-
图形界面应用: C++ 被用于开发桌面应用程序、图形用户界面(GUI)和各种图形软件。
-
网络编程: C++ 的网络库和框架使其成为开发网络应用和服务器端程序的理想选择。
C++ 以其高性能、灵活性和底层控制能力,成为许多领域中首选的编程语言之一。
一、命名空间
命名空间(Namespace)是一种在C++中用于组织和封装代码的机制。它允许将一组相关的标识符(如变量、函数、类等)封装在一个逻辑单元中,目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。命名空间可以被定义在全局作用域或其他命名空间内。
1.1 命名空间的定义
语法:
namespace 名称 {
// 命名空间内的代码块
// 变量、函数、类的声明和定义等
}
- 1
- 2
- 3
- 4
-
命名空间中可以定义变量/函数/类型
namespace Namespace { // 变量 int rand = 10; //函数 int Add(int left, int right) { return left + right; } //类型 struct Node { struct Node* next; int val; }; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
-
命名空间可以嵌套
// 外层命名空间 namespace N1{ int a; int Add(int left, int right){ return left + right; } // 内层命名空间 namespace N2{ int c; int Sub(int left, int right){ return left - right; } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
-
C++ 允许命名空间的逐渐定义,而多个定义会在编译时进行合并,确保同一命名空间内的内容是唯一的。
在这个例子中,num3 命名空间的两个部分分别定义在不同的文件中,但它们最终会在编译时合并成为一个包含两个成员的命名空间。// 文件1.cpp namespace num3 { int rand = 3; } // 文件2.cpp namespace num3 { int y = 4; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
1.2 命名空间的使用
以下是一些常见的命名空间的使用场景和实践:
-
避免全局命名冲突: 命名空间允许将相关的标识符封装在一个独立的逻辑单元中,从而避免与其他代码中的相同名称的标识符发生冲突。这对于大型项目或多人协作的代码库非常重要。
// 定义命名空间 namespace MyNamespace { int myVariable; void myFunction(); class MyClass { /* ... */ } }
- 1
- 2
- 3
- 4
- 5
- 6
-
组织和模块化代码: 命名空间使得代码更加模块化和易于组织。通过将相关的功能放置在同一个命名空间中,代码结构更加清晰,对于代码的理解和维护更加方便。
// 在项目中的不同文件中使用同一个命名空间 // File1.cpp namespace Math { int add(int a, int b); } // File2.cpp namespace Math { int subtract(int a, int b); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
防止全局污染: 将标识符限定在命名空间中,避免了全局命名空间的污染。这样有助于控制和隔离代码的影响范围。
// 不使用命名空间的情况 int globalVariable; // 使用命名空间 namespace MyNamespace { int myVariable; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
-
版本控制: 在不同的版本中,可以使用命名空间来隔离和管理不同版本的功能或接口,以确保向后兼容性或易于升级。
// Version 1 namespace MyApp_v1 { void someFunction(); } // Version 2 namespace MyApp_v2 { void someFunction(); void newFunction(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
解决命名冲突: 当使用第三方库或合作开发时,可能会遇到不同部分使用相同名称的情况。使用命名空间可以帮助解决这些命名冲突。
// 第三方库的命名空间 namespace ExternalLibrary { void commonFunction(); } // 项目的命名空间 namespace MyProject { void commonFunction(); void myFunction(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
1.3 命名空间的访问
命名空间的成员可以通过作用域解析运算符 ::
进行访问。通过 ::
运算符,你可以指定要访问的命名空间的名称,从而使用其中的变量、函数或类等成员。
以下是一些命名空间访问的示例:
通过 MyNamespace::
,我们可以访问命名空间 MyNamespace
中的变量、函数和类。这样的方式可以有效地避免全局命名冲突,同时使得代码更具可读性。
// 定义命名空间
namespace MyNamespace {
int myVariable = 42;
void myFunction() {
// 函数定义
}
class MyClass {
// 类定义
}
}
int main() {
// 访问命名空间中的变量
int value = MyNamespace::myVariable;
// 访问命名空间中的函数
MyNamespace::myFunction();
// 访问命名空间中的类
MyNamespace::MyClass myObject;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
通过 NS1:: 和 NS2:: 分别指定了要访问的命名空间,从而区分了两个同名的变量。
namespace NS1 {
int x;
}
namespace NS2 {
int x;
}
int main() {
// 访问不同命名空间中的同名变量
int value1 = NS1::x;
int value2 = NS2::x;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
当你有一个嵌套的命名空间结构时。在这个示例中,OuterNamespace
是外层命名空间,InnerNamespace
是内层命名空间。通过作用域解析运算符 ::
,我们可以访问这两个命名空间中的变量。value1
获取了外层命名空间的变量值,而 value2
获取了内层命名空间的变量值。
// 外层命名空间
namespace OuterNamespace {
int outerVariable = 42;
// 内层命名空间
namespace InnerNamespace {
int innerVariable = 10;
}
}
int main() {
// 访问外层命名空间的变量
int value1 = OuterNamespace::outerVariable;
// 访问内层命名空间的变量
int value2 = OuterNamespace::InnerNamespace::innerVariable;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
二、C++输入&输出
C++ 中的输入和输出操作符是 <<
(输出操作符)和 >>
(输入操作符)。这些操作符用于在控制台或文件中进行输入和输出操作。
std
是C++标准库(Standard Library)的命名空间,cout
、cin
和 endl
是C++标准库中命名空间std
的对象,它们属于输入输出流相关的头文件
输出操作符 <<
在C++中,<<
是输出流操作符,用于将右侧的数据插入到左侧的输出流中(通常是标准输出流,即屏幕)。std::cout
是标准输出流对象,<<
将数据输出到流中。多个 <<
操作符可以串联使用,将多个数据输出到同一个流中。
#include
int main() {
int number = 42;
std::cout << "The value of number is: " << number << std::endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
输入操作符 >>
在C++中,>>
是输入流操作符,用于从流中读取数据。std::cin
是标准输入流对象,>>
从流中读取数据并存储在 number
变量中。
#include
int main() {
int number;
std::cout << "Enter a number: ";
std::cin >> number;
std::cout << "You entered: " << number << std::endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
换行符和刷新输出缓冲区
std::endl
是C++标准库中的一个操作符,用于表示换行符和刷新输出缓冲区。它实际上是一个函数模板,定义在头文件 中,并属于 std
命名空间。
使用 std::endl
可以在输出流中插入一个换行符,并确保输出缓冲区被刷新。刷新输出缓冲区是为了确保立即将输出内容显示在屏幕上,而不是等待缓冲区被填满或遇到显式的刷新操作。等效地,也可以使用 '\n'
来表示换行符。
#include
int main() {
int number = 42;
// 使用 std::endl 插入换行符并刷新输出缓冲区
std::cout << "The value of number is: " << number << std::endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
关键字 using
使用 using
关键字可以展开命名空间中的标识符,使其在代码中更直接可用,而不需要显式加上命名空间的前缀。
以下是使用 using
展开命名空间的示例:
#include
// 使用 using 展开命名空间
using namespace std;
int main() {
// std::cout 可以直接使用为 cout
cout << "Hello, World!" << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
需要注意的是,虽然 using namespace std;
提供了方便,但在大型项目中或与其他库集成时,可能会引入潜在的命名冲突。为了避免这些问题,一种更安全的方式是只使用需要的特定标识符,而不是整个命名空间。例如:
#include
// 使用 using 展开需要的标识符
using std::cout;
using std::endl;
int main() {
// 直接使用展开后的标识符
cout << "Hello, World!" << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
三、缺省参数
3.1 概念
缺省参数(default parameters)允许在函数声明中为一个或多个参数指定默认值。这意味着在调用函数时,如果没有提供相应的参数值,将使用该参数的默认值。
void Func(int a = 0){
cout << a << endl;
}
int main(){
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
3.2 缺省参数分类
函数的参数可以全部使用缺省参数,也可以部分使用缺省参数,实现全缺省参数和半缺省参数的效果。
全缺省参数:
printInfo
函数的所有参数都使用了缺省参数,可以在调用时不提供任何参数值,或者提供部分或全部参数值。
#include
// 函数声明时全部使用缺省参数
void printInfo(int a = 1, int b = 2, int c = 3);
int main() {
// 调用函数时不提供任何参数值,使用了所有参数的默认值
printInfo();
// 提供部分参数值
printInfo(4);
// 提供所有参数值,不使用默认值
printInfo(7, 8, 9);
return 0;
}
// 函数定义时也使用全部缺省参数
void printInfo(int a, int b, int c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
半缺省参数:
半缺省参数必须从右往左依次来给出,不能间隔着
#include
// 函数声明时部分使用缺省参数
void printInfo(int a, int b = 2, int c = 3);
int main() {
// 调用函数时提供所有参数值,不使用默认值
printInfo(7, 8, 9);
// 提供部分参数值
printInfo(4);
// 调用函数时不提供任何参数值,使用了第一个参数的默认值
printInfo();
return 0;
}
// 函数定义时也使用部分缺省参数
void printInfo(int a, int b, int c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
需要注意的是, 缺省参数应避免函数声明和定义中同时出现,当缺省参数在函数声明和定义中同时出现时,编译器会面临二义性的问题
如果缺省参数在函数声明和定义中同时出现:
-
编译阶段:
- 编译器首先会处理声明,其中包含缺省参数的值。这个信息在汇编代码中被保存。
- 接着,编译器会处理定义,其中也包含缺省参数的值。
-
链接阶段:
- 链接器需要解析符号,即函数名和参数列表。但现在存在两个相同的函数符号,一个来自声明,一个来自定义。
- 缺省参数的值在编译阶段已经确定,但由于声明和定义提供了相同的信息,链接器无法准确地选择应该使用哪个缺省参数的值。
因此,在声明和定义中同时出现缺省参数会导致链接阶段的二义性,编译器无法确定应该使用哪个值,从而引发错误。为了避免这种情况,建议在声明中指定缺省参数的值,而在定义中不再提供。这样,在链接阶段,编译器就能够使用声明中的默认值。
四、函数重载
概念
函数重载是指在同一个作用域内定义多个同名函数,但它们的参数列表 (包括参数的类型、顺序或数量)不同。
#include
using namespace std;
// 重载函数,按类型
void print(int x) {
cout << "Integer: " << x << endl;
}
void print(double x) {
cout << "Double: " << x << endl;
}
// 重载函数,参数个数不同
void display(int x) {
cout << "Display Integer: " << x << endl;
}
void display(int x, double y) {
cout << "Display Integer and Double: " << x << ", " << y << endl;
}
// 重载函数,参数顺序不同
void show(double x, int y) {
std::cout << "Show Double and Integer: " << x << ", " << y << endl;
}
void show(int x, double y) {
cout << "Show Integer and Double: " << x << ", " << y << endl;
}
int main() {
// 函数按类型重载
print(42); // 调用第一个 print 函数
print(3.14); // 调用第二个 print 函数
// 函数参数个数不同重载
display(10); // 调用第一个 display 函数
display(20, 3.5); // 调用第二个 display 函数
// 函数参数顺序不同重载
show(2.5, 15); // 调用第一个 show 函数
show(7, 4.2); // 调用第二个 show 函数
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
函数重载的原理
C++中使用名称修饰来实现函数重载。而在C语言中,并不使用名称修饰的方式,函数名是直接暴露的,因此C语言不支持函数重载
函数重载的编译阶段和链接阶段:
-
编译阶段:
-
函数定义: 在源代码中,定义了多个同名但参数列表不同的函数,即进行函数重载。例如:
void print(int x); void print(double x);
- 1
- 2
-
名称修饰: 编译器在编译阶段根据函数的名称和参数列表生成唯一的名称修饰后的标识符。这个过程被称为名称修饰或符号修饰。
- 例如,生成的标识符可能为
_Z5printi
和_Z5printd
。
- 例如,生成的标识符可能为
-
生成目标文件: 编译器将源代码编译成目标文件(通常是
.o
文件),其中包含了函数的名称修饰后的标识符。
-
-
链接阶段:
-
符号表生成: 在链接阶段,链接器将汇总所有目标文件中的符号,创建一个符号表。符号表记录了函数和变量名与其对应的地址或标识符。
- 符号表可能包含了
_Z5printi
和_Z5printd
这样的名称修饰后的标识符。
- 符号表可能包含了
-
解析函数调用: 当程序中有函数调用时,链接器查找符号表,尝试解析函数名。
- 如果有一个唯一匹配的标识符,链接器确定正确的函数地址或标识符。
-
写入可执行文件: 链接器将正确的函数地址或标识符写入最终的可执行文件中,确保在运行时能够正确调用相应的函数。
-
-
运行时:
- 函数调用: 当程序运行时,根据函数调用,执行相应的函数。函数的具体实现取决于编译时和链接时确定的地址或标识符。
函数重载的过程涉及编译阶段和链接阶段:编译阶段中,编译器根据函数的名称和参数列表生成唯一的名称修饰后的标识符,汇总所有标识符并创建一个符号表;链接阶段中,链接器通过符号表解析函数调用,将正确的函数地址或标识符写入最终的可执行文件,确保在运行时能够正确调用相应的函数。
如果你喜欢这篇文章,点赞?+评论+关注⭐️哦!
欢迎大家提出疑问,以及不同的见解。
评论记录:
回复评论: