🔥个人主页: Quitecoder
🔥专栏:c++笔记仓
朋友们大家好!本篇内容我们进入一个新的阶段,进入c++的学习!希望我的博客内容能对你有帮助!
目录
- 1.c++关键字
- 2.第一个c++代码
- 3.命名空间
- 3.1 namespace
- 3.2命名空间使用
- 4.c++中的输入输出
- 5.缺省参数
- 编译与链接过程
- 函数与文件的关系
- 6.函数重载
- 6.1函数重载的的原理:名字修饰
1.c++关键字
C++总计63个关键字,C语言32个关键字
C++是一种与C语言紧密相关的编程语言。尽管它继承了C语言的许多特点,但C++引入了面向对象编程等概念,并增加了一些自己的特性和关键字来支持这些特性。比较C++和C语言的关键字,我们可以发现以下特征:
增加的关键字: C++增加了一些关键字来支持面向对象编程(如类、继承、多态等)和模板编程。例如,class
,public
,protected
,private
,virtual
,friend
,template
,typename
等。这些关键字没有在C语言中。
类型增强:C++增加了一些用于类型安全和方便的关键字,如bool
,true
,false
,using
,namespace
等。
异常处理:为了支持异常处理,C++引入了try
,catch
,throw
等关键字。
新的转换操作符:C++提供了static_cast
,dynamic_cast
,const_cast
和reinterpret_cast
等关键字进行类型转换,这是C语言中所没有的。
增强的存储类说明符:C++引入了mutable
和thread_local
等存储类说明符。
模板编程:为了支持泛型编程,C++增加了template
和typename
关键字。
新增运算符:C++还定义了如new
,delete
等用于动态内存管理的关键字,这些在C中通常通过库函数如malloc和free来实现。
特殊成员函数关键字:C++还有如default
和delete
等关键字,用于特殊成员函数的声明,这样设计是为了提供更好的控制。
C++相比C语言增加的关键字主要围绕面向对象编程、模板编程、异常处理、类型安全和内存管理等方面。这些新增加的特性和关键字使得C++成为一种更复杂但功能更强大的语言,这些目前我们不做过多解释,后期会逐个遇到
2.第一个c++代码
我们来看第一个c++代码:
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
我们逐个来看:
#include<iostream>
:这串代码是C++程序中的预处理指令,它的作用是告诉预处理器在实际编译之前包含标准输入输出流库iostream。这个库是C++标准库的一部分,为程序提供了输入输出功能,主要通过定义了一些流对象,例如std::cin
、std::cout
我们可以发现在C++标准库中,标头文件通常不使用传统的.h后缀。这是C++标准制定时约定的一种风格,用来区分C++标准库的头文件和C风格或其他库的头文件。C语言的标准库头文件和一些旧的C++代码可能仍然使用.h后缀(例如stdio.h),但C++标准库头文件不遵循这一约定
using namespace std;
: 这行代码是使用命名空间std的声明。std是标准C++库中定义的命名空间,其中包括了诸如cout、cin等通过这条声明,可以直接使用cout而不是std::cout
来引用标准输出流对象,这个后面会讲到cout<<"hello world"<<endl;
- cout是一个输出流对象,用于发送数据到标准输出设备(如屏幕)
- <<是插入操作符,用于将后面的数据发送到前面指定的流对象(这里是cout)
- "hello world"是要输出的字符串。
- endl是一个操控符,用于在输出流中插入一个换行符,然后刷新输出缓冲区,使得输出立即出现在目标设备上。这在某些情况下比简单使用\n换行符更有用,因为它确保了数据的即时输出
简单的分析完后,我们进行讲解
3.命名空间
在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存
在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的
在C语言中,实际上没有命名空间这一概念,所有的标识符(包括变量名、函数名等)都位于同一个全局命名空间中。因此,当两个不同的库或代码片段中存在同名的标识符时,就会发生命名冲突。以下是一个简单的示例说明这种情况:
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
这里存在的问题是,rand 是 <stdlib.h> 中已经定义的一个函数,该函数用于生成伪随机数。然而,在代码中,又定义了一个全局变量 rand 并赋值为10。这导致当在 main 函数中引用 rand 时,实际上引用的是定义的全局变量,而不是标准库中的 rand() 函数。
这正是命名冲突的一个例子:一个是标准库 <stdlib.h> 中的函数 rand(),另一个是用户定义的全局变量 rand。由于C语言中缺乏命名空间机制,这两个同名的实体会发生冲突
C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
3.1 namespace
命名空间(Namespace)是C++中一种极为重要的特性,用来避免命名冲突,并且组织代码,使其易于维护和扩展。命名空间提供了一个范围,在这个范围内的名字(可以是变量、函数、结构体、类等)是可见的,但在范围外则不是。这允许开发者在不同的命名空间中使用相同的名字,而不会造成冲突。这特别对大型项目或者在集成多个库的时候非常有用
定义命名空间,需要使用到namespace
关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员
namespace myrand
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
- myrand为命名空间的名字
- 命名空间可以包含变量、函数、结构、类等多种类型的成员。myrand命名空间内定义了一个名为rand的整型变量,并初始化值为10,这样做的好处是可以避免命名冲突
命名空间也可以嵌套定义,例如:
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
若我们再定义一个命名空间,取名仍为N1,编译器在编译时会将两个命名空间合并
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
3.2命名空间使用
方法一:加命名空间名称及作用域限定符
首先我们来介绍一个符号::
,由两个冒号组成的一个符号叫做域作用限定符,
注意,下面代码均在.cpp
后缀的文件实现的
例如代码:
#include<stdio.h>
int a = 1;
int main()
{
int a = 20;
printf("%d\n", a);
return 0;
}
这串代码,打印a时,首先会访问局部变量里面的a,如果我们想访问全局变量中的a,则需要使用全局命名空间操作符::
来访问全局变量
::
前缀指示编译器查找全局作用域中的a。因此,即使在main函数内部有一个同名的局部变量,使用::a
还是可以访问到全局变量a,打印出的值为1
我们也可以访问自定义空间中的变量:
namespace s1 {
int a = 1;
}
namespace s2 {
int a = 2;
}
int main()
{
int a = 20;
printf("%d\n", a);
printf("%d\n", s1::a);
printf("%d\n", s2::a);
return 0;
}
命名空间域将a封起来放在全局变量中
编译器使用变量时,会进行搜索,首先会搜索局部域,再搜索全局域,我们想访问命名空间域里面的变量,就需要加命名空间名称及作用域限定符
这种特性在C++中非常有用,尤其是当局部变量的名称可能会与全局变量或者在其他命名空间中的变量重名时,可以通过这种方式明确指明想要使用的是哪个作用域中的变量
方法二:使用using将命名空间中某个成员引入
namespace N1
{
int a=2;
int b=3;
}
using N1::b;
int main()
{
printf("%d\n",N1::a);
printf("%d\n",b);
return 0;
}
这里,N1命名空间包含了两个全局变量a和b,它们的作用域被限制在了N1命名空间内部。这意味着它们不能被直接访问,除非使用其命名空间名作前缀
接下来,通过using
声明导入了N1命名空间中的b变量:
using N1::b;
这个声明使得在using声明所在作用域(在这个例子中,是全局作用域)内,可以直接访问b而不需要N1::
前缀。这种方式对于频繁访问某个命名空间中的特定成员而不想每次都写全命名空间名时非常有用
printf("%d\n", N1::a); // 输出2,通过完全限定名访问N1中的a
printf("%d\n", b); // 输出3,通过using声明访问N1中的b
- 对于
N1::a
的访问,因为没有用using语句声明a,所以需要使用完全限定名N1::a
来访问它 - 对于b,因为前面有
using N1::b;
声明,所以可以直接访问,不需要命名空间前缀
方法三:使用using namespace 命名空间名称 引入
namespace N1
{
int a=2;
int b=3;
}
using namespace N1;
int main()
{
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
using namespace N1;
这一语句告诉编译器,接下来的代码中如果遇到某个符号的名称与N1命名空间内的某个成员的名称匹配,就将这个符号解析为该命名空间内的成员,这使得在后续代码中,你可以不使用N1::
前缀就直接访问a和b
4.c++中的输入输出
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
再看这串代码
std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
说明:
-
#include <iostream>
这一行告诉编译器包含标准输入输出流库。这个库提供了输入输出的设施,其中就包括了cout, 使用cout
标准输出对象(控制台)和cin
标准输入对象(键盘)时,必须包含< iostream >
头文件以及按命名空间使用方法使用std -
cout是C++标准库中的一个对象,被封装在名为std的命名空间中,这里我们使用using namespace std,使得输出不需要加前缀,如果不用这串代码,则需要指定命名空间
输出格式如下:
#include<iostream>
int main()
{
std::cout<<"hello world"<<std::endl;
return 0;
}
cout
通常用于向标准输出设备,通常是命令行(控制台)输出文本,endl是另一标准库对象,用于插入换行符并刷新输出缓冲区,也属于std命名空间
下面看这串代码:
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
cin>>a
;:这行代码从标准输入接受一个整数,并将其存储在变量a中。cin会根据提供的变量类型自动解释输入数据。我们假设用户输入了一个整数- cin>>b>>c;:这行代码首先从标准输入接受一个双精度浮点数,并将其存储在变量b中,接着接受一个字符并存储在c中。这演示了如何通过一个表达式从cin连续读取多个值
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型
- <<是流插入运算符,>>是流提取运算符
5.缺省参数
在C++中,缺省参数(也称为默认参数)是函数或方法参数声明中所指定的默认值。如果在调用函数时未提供相应的参数,那么将自动使用这个默认值。这使得函数调用更加灵活
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
这里打印结果为:
0
10
缺省参数有以下类型:
全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注意,这里传参是按照顺序来的,不可以跳过某个参数传参
半缺省参数
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
半缺省参数必须从右往左依次来给出,不能间隔着给
//a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
在C++中,当一个函数有缺省参数(默认参数)时,这个规则确保了程序的清晰性与一致性,避免了潜在的混淆。缺省参数意味着在函数调用中,如果没有提供某些参数,那么将自动使用这些参数的默认值。这句话的含义是,对于给定的函数,其缺省参数应该只在函数声明或定义中的一处指定,而不是两处同时指定
理解这句话的关键在于区分声明和定义的概念:
-
函数声明:告诉编译器函数的名称、返回类型以及参数列表(类型、顺序和数量),但不涉及函数的具体实现。函数声明经常出现在头文件(
.h
)中 -
函数定义:提供了函数的实际实现,它包括函数的主体,即函数被调用时将执行的具体代码。函数定义包含了函数声明的所有信息,并加上了函数体
为什么不能同时出现
如果在函数的声明和定义中都指定了缺省参数,可能会导致不一致性,使得理解和维护代码变得更加困难,编译器也可能不确定应该使用哪个版本的默认值,尤其是当声明和定义位于不同的文件时,为了避免这种情况,C++标准规定了缺省参数应当只在一个地方指定:
- 如果函数声明在头文件中进行,那么就在头文件中的声明处指定缺省参数;
- 如果函数没有在头文件中声明(例如,完全在一个
.cpp
文件内定义),那么就在函数定义处指定缺省参数
最佳实践
最佳实践是在函数的声明中指定缺省参数,而在函数的定义中则省略这些默认值
// 函数声明,在头文件中
void example(int a, float b = 3.14);
// 函数定义,在源文件中
void example(int a, float b) {
// 函数体
}
在这个例子中,example
函数的缺省参数只在函数声明中给出,而在定义时省略了默认值。这符合C++的最佳实践
当函数声明在头文件中进行,并在头文件中指定缺省参数,这与头文件的工作原理及C++编译过程有关
当函数声明在头文件中进行,并在头文件中指定缺省参数,这与头文件的工作原理及C++编译过程有关:
-
预处理阶段:在这个阶段,编译器处理源代码中的预处理指令,如
#include
(将头文件内容展开至引用位置)、#define
等。如果在头文件中指定了缺省参数,当进行#include
预处理时,这些默认值也会被一并复制到每个包含了该头文件的源文件中,这确保了源文件在进入编译阶段时已经拥有了完整的函数声明信息 -
编译阶段:编译器将预处理后的源代码转换成目标代码(通常是机器代码的一种中间形态)。如果函数的缺省参数在头文件中被声明,那么每个包含了该头文件的源文件都能正确地编译函数调用,因为它们都"看到"了相同的带有缺省参数的函数声明
-
链接阶段:链接器将多个对象文件(目标代码)和库一起链接成最终的可执行文件。由于缺省参数已经在头文件中声明,并且该头文件被所有需要的源文件正确地包含,链接器不需要关心默认值的问题,因为这些默认值不影响函数的链接过程
我们这里扩展一下:
假如我现在有三个文件,stack.h包含函数的声明,stack.c包含函数的定义,test.c是一个测试函数所用的文件,先简单说明一下这些文件的作用:
- stack.h(头文件):包含函数声明(也可能包含类型定义、宏定义等)。它的主要目的是提供一个接口的定义,以便其他文件在使用这些函数时,编译器能够了解到它们的存在及其接口
- stack.c(源文件):包含函数的具体实现。这里编写了在
stack.h
中声明的函数的代码体 - test.c(源文件):用于测试的代码文件,它会使用
stack.h
中声明的函数
编译与链接过程
-
预处理:对于每个
.c
文件,编译过程从预处理开始。预处理器会处理以#
开头的指令,例如#include "stack.h"
会将stack.h
中的内容文本上粘贴到stack.c
和test.c
文件中,这样stack.c
和test.c
就可以看到这些函数声明了 -
编译:编译器接着编译每个
.c
源文件,将它们转换成目标代码(通常是机器代码的一种中间形态,称为目标文件,扩展名为.o
或.obj
)。此时,编译器确保源代码符合语法规则,对每个源文件进行类型检查,确保所有函数调用都符合其声明,但还不解决跨文件的函数引用问题。例如,stack.c
被编译成stack.o
,test.c
被编译成test.o
-
链接:一旦所有的源文件被编译成目标文件,链接器(linker)负责将这些目标文件以及必要的库文件链接成一个单一的可执行文件。在链接过程中,如果
test.c
(对应的是test.o
)调用了stack.c
中(对应的是stack.o
)的函数,链接器负责“修补”这些调用,使得test.o
中的调用可以正确地连接到stack.o
中定义的函数上,链接器确保所有外部引用都能正确解析到它们所引用的实体。
函数与文件的关系
- 在
stack.h
中声明的函数,让其他源文件知道这些函数的存在、它们的参数以及返回值类型。stack.h
扮演了接口的角色。 stack.c
提供了stack.h
中声明的函数的具体实现。test.c
作为使用这些函数的客户端代码,通过#include "stack.h"
能够调用这些函数。- 编译过程中,
test.c
和stack.c
分别被编译成中间的目标文件。这些目标文件中的函数调用尚未解析到具体的地址 - 在链接过程,链接器解析这些调用,使得从
test.o
中的调用可以正确地定位到stack.o
中的函数定义,从而生成一个完整的可执行文件,所有的函数调用都被正确地解析和连接,这个地址修正的过程也叫做重定位
接下来我们所讲解的函数重载与上述内容也有所关联
6.函数重载
函数重载是C++语言的一个特性,它允许在同一作用域内声明几个具有相同名字的函数,只要这些函数的参数列表不同。这个机制让程序员可以为执行类似操作但需要处理不同数据类型或参数数量的函数提供统一的接口
参数不同:
#include <iostream>
using namespace std;
void print(int i) {
cout << "Printing int: " << i << endl;
}
void print(double f) {
cout << "Printing float: " << f << endl;
}
int main() {
print(1); // 调用 print(int)
print(3.14); // 调用 print(double)
return 0;
}
参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
参数顺序不同:
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
6.1函数重载的的原理:名字修饰
C++支持函数重载的原理,在很大程度上依赖于一种被称为**名字修饰(Name Mangling)**的过程。这种机制使得编译器能够区分同名但参数列表不同的函数,从而支持函数重载
名字修饰是什么?
名字修饰是编译器自动进行的一种处理过程,它将C++源代码中的函数名和变量名转换成包含更多信息的唯一标识符。这些信息通常包括函数的参数类型、参数数量等,甚至可能包括所属的类名(对于类成员函数),通过这种方式,每个重载的函数都会被赋予一个独一无二的名字,确保链接器在最后链接程序的时候能够区分它们
C++中允许函数重载,也就是允许同一个作用域内存在多个同名函数,只要它们的参数列表不同。但在编译成目标代码后,所有的函数名和变量名都必须区分开来,确保每个函数调用都能显式地映射到正确的函数体上。名字修饰通过在函数名中编码函数参数类型等信息,实现了这一点
名字修饰实例
比如,考虑下面的C++函数重载:
void print(int i);
void print(double d);
在经过编译器处理后,这些函数可能会分别被修饰为(名字修饰的具体结果依赖于编译器):
print(int)
可能被修饰为_print_i
print(double)
可能被修饰为_print_d
这样,尽管这两个函数原名相同,但在编译器处理后它们获得了不同的名字,使得编译后的代码中对这些函数的引用能够清晰地区分开来
名字修饰使得C++能够有效地支持函数重载和模板等功能,虽然这种机制在编程过程中对程序员是透明的,但理解其背后的原理对于深入掌握C++语言是有帮助的。同时,这也是C++与其他语言例如C的一个重要区别:C语言不支持函数重载,部分原因就在于它没有采用类似的名字修饰机制
我们来看linux环境下不同编译器编译相同代码的结果:
采用C语言编译器编译后结果:
在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变
采用C++编译器编译后结果
在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分
本节内容到此结束,感谢大家阅读!!