目录
一、关键字
二、命名空间
问题引入(问题代码):
域的问题
1.::域作用限定符 的 用法:
2.域的分类
3.编译器的搜索原则
命名空间的定义
命名空间的使用
举个🌰栗子:
1.作用域限定符指定命名空间名称
2. using 引入命名空间中的成员 即 展开命名空间中某一个
3. usinng namespace 命名空间名称 展开命名空间
三、C++输入、输出
四、缺省参数
概念
全缺省参数
半缺省参数
实践中的应用场景🌰举个例子:
声明和定义分离
回顾 声明 和定义的概念
再来分析上述程序
理解编译与链接的过程
理解函数与文件的关系
五、函数重载
代码示例:
C++是如何支持函数重载的?
函数名修饰
一、关键字
asm | do | if | return | try | continue |
---|---|---|---|---|---|
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
增加的关键字: 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等关键字,用于特殊成员函数的声明,这样设计是为了提供更好的控制。
二、命名空间
问题引入(问题代码):
下面代码存在命名冲突 : rand变量 和头文件<stdlib.h>中声明的函数 rand() 名字相同 导致冲突。
#include<stdio.h>
#include<stdlib.h> /*rand*/
int rand = 0;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespaceguan来解决
int main()
{printf("%d\n",rand);return 0;
}
域的问题
1.::域作用限定符 的 用法:
限定符左边是哪一个域名 就限定了访问该变量的范围
左边是空 默认是全局域
2.域的分类
-
全局域
-
局部域:如果不用限定符,默认访问局部域 局部优先
-
命名空间域:为了防止命名冲突 eg.全局定义两个同名变量 ,防止重定义,C++提出就用关键字namespace把他们定义在不同命名空间域中。
-
类域
注意:
全局域、局部域既会影响生命周期,也会影响访问。命名空间只影响访问
3.编译器的搜索原则
1️⃣当前局部域 2️⃣全局域 3️⃣如果指定了,直接去指定域搜索
命名空间的定义
正常定义
// 正常的命名空间定义
namespace hhh
{// 命名空间中可以定义变量/函数/类型int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};}
嵌套定义
举个栗子🌰:
namespace aaa
{namespace bbb{void Push(){cout<<"zs"<<endl;}}namespace ccc{void Push(){cout<<"yyy"<<endl;}}
}
int main()
{//嵌套定义在命名空间的同名函数 各自调用bit::bbb::Push();bit::ccc::Push();return 0;
}
ps:命名空间可以重名,编译器会把他们合并,只要命名空间内部不冲突就可以
命名空间的使用
命名空间到底该如何使用?
举个🌰栗子:
namespace yyy
{//命名空间中定义 变量 / 函数 /类型int a = 0;int b= 1;int Add(int left,int right){return left+right;}struct Node{struct Node* next;int val;};
}
1.作用域限定符指定命名空间名称
//指定访问
int main()
{//::作用域限定符printf("%d\n",yyy::a);return 0;
}
2. using 引入命名空间中的成员 即 展开命名空间中某一个
//展开一个
using yyy::b;
int main()
{printf("%d\n",yyy::a);//不可以 因为此时只展开了一个成员变量printf("%d\n",b);
}
3. usinng namespace 命名空间名称 展开命名空间
展开命名空间 影响的是 域的搜索规则。不展开命名空间,默认情况编译器只会在局部域、全局域搜索。展开命名空间就可以在命名空间里搜索。
//展开全部
using namespace yyy;
int main()
{printf("%d\n",yyy::a);//指定去该命名空间找变量aprintf("%d\n",b)
}
注意:
1. 日常练习展开为了方便使用可以展开std,实际工程实践中慎重使用!
2.展开命名空间 不是 等同于引入全局变量!
3.展开命名空间 跟 包含头文件 也有本质区别,包含头文件 在预处理过程中本质是拷贝头文件的内容
三、C++输入、输出
解释Hello world代码
//包含标准输入输出流库
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;int main(){//cout和cin是全局的流对象,细说分别是ostream和istream类型的对象// <<是流插入运算符,>>是流提取运算符//endl是C++符号,表示endline换行//他们都包含在包含<iostream>头文件中cout<<"Hello world!!!"<<endl;return 0;}
说明:使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件 以及按命名空间使用方法使用std。
补充:std命名空间的使用习惯
1.日常练习:直接展开 using namespace std
2.项目开发:std::cout 使用时指定命名空间 + using std::cout 展开常用库对象
C++ 输入输出 自动识别变量类型
-
示例代码:
#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中。
四、缺省参数
-
概念
声明或定义函数时为函数的参数指定缺省值。缺省值就是给形参设置一个默认值。调用函数时,如果没有指定实参,则使用参数的默认值。
缺省值必须是 常量或者全局变量。一般使用常量。
void Func(int a = 0) {cout<<a<<endl; } int main() {Func(); //没有传参 使用参数默认值 Func(10); //传参时 使用指定的实参return 0; }
-
全缺省参数
void Func(int a = 10, int b = 20, int c = 30) {cout<<"a = "<<a<<endl;cout<<"b = "<<b<<endl;cout<<"c = "<<c<<endl; } 调用Func()时,可以这样给参数int main() {Func(1,2,3);Func(1,2);Func(1);Func();//注意:不可以跳越传值//Func(,1,2);return 0; }
-
半缺省参数
注意:只能从右往左连续给缺省值,这样调用保证传的实参顺序不存在歧义
void Func(int a, int b = 20, int c = 30) {cout<<"a = "<<a<<endl;cout<<"b = "<<b<<endl;cout<<"c = "<<c<<endl; } //调用 同样不能跳越给 int main() {Func(1,2,3);Func(1,2);Func(1); }
实践中的应用场景🌰举个例子:
struct Stack
{int* a;int size;int capacity;//...
};
//StackInit()改造为半缺省函数 使得可以适用更多的需要开辟空间的场景
void StackInit(struct Stack* ps,int n=4)
{ps->a=(int*)malloc(sizeof(int)*n);
}
int main()
{struct Stack st1;//缺省参数 使得函数可以适应不同场景 // 1、确定要插入100个数据StackInit(&st1, 100); // call StackInit(?)
// 2、只插入10个数据struct Stack st2;StackInit(&st2, 10); // call StackInit(?)
// 3、不知道要插入多少个 //这时就可以使用函数定义里提供的 参数缺省值 //不知道插入多少个 可以先初始化四个空间struct Stack st3;StackInit(&st3);
return 0;
}
-
声明和定义分离
回顾 声明 和定义的概念
-
函数声明:告诉编译器函数的名称、返回类型以及参数列表(类型、顺序和数量),但不涉及函数的具体实现。函数声明经常出现在头文件(
.h
)中 -
函数定义:提供了函数的实际实现,它包括函数的主体,即函数被调用时将执行的具体代码。函数定义包含了函数声明的所有信息,并加上了函数体
//Stack.h 声明 struct Stack {int* a;int size;int capacity;//... }; void StackInit(struct Stack* ps,int n=4);//*注意 必须在声明中给出缺省值 void StackPush(struct Stack* ps,int x); //Stack.cpp 定义 void StackInit(struct Stack* ps,int n)//*注意声明和定义中缺省值不能同时给 {ps->a=(int*)malloc(sizeof(int)*n); } void StackPush(struct Stack* ps , int x) {} //Test.cpp #include"Stack.h" int main() {struct Stack st1;// 1、确定要插入100个数据StackInit(&st1, 100); // call StackInit(?)//此时包含了头文件,Test.cpp只有函数声明 用这个函数的名字找到该函数的地址 编译阶段会检查调用该函数是否存在匹配的函数,经过检查 匹配// 2、只插入10个数据struct Stack st2;StackInit(&st2, 10); // call StackInit(?)// 3、不知道要插入多少个 struct Stack st3;StackInit(&st3);return 0; }
但是试想一下,1️⃣如果缺省值只在函数定义中给出,编译阶段 无法用这个函数的名字找到该函数的匹配 ,因为调用传参跟函数声明并不匹配。另一种情况,2️⃣如果在函数的声明和定义中都指定了缺省参数编译器也可能不确定应该使用哪个版本的默认值为了避免这种情况,C++标准规定了缺省参数应当只在一个地方指定:
-
如果函数声明在头文件中进行,那么就在头文件中的声明处指定缺省参数;
-
如果函数没有在头文件中声明(例如,完全在一个
.cpp
文件内定义),那么就在函数定义处指定缺省参数
综上,
1️⃣在项目中,声明和定义应当分离,缺省值一定要在函数声明中给出!因为,编译阶段只有函数声明,从而保证编译阶段是没有问题的。
2️⃣声明和定义分离,导致编译阶段无法找到函数的定义,没有函数的地址。
-
-
再来分析上述程序
-
理解编译与链接的过程
1️⃣预处理阶段 :展开头文件、宏替换、条件编译、删除注释
对于每个
.c
文件,编译过程从预处理开始。预处理器会处理以#
开头的指令,例如#include "stack.h"
会将stack.h
中的内容文本上粘贴到stack.c
和test.c
文件中,这样stack.c
和test.c
就可以看到这些函数声明了2️⃣编译:检查语法➡️生成汇编代码
编译器接着编译每个.c源文件,将它们转换成目标代码(通常是机器代码的一种中间形态,称为目标文件,扩展名为.o或.obj)。此时,编译器确保源代码符合语法规则,对每个源文件进行类型检查,确保所有函数调用都符合其声明,但还不解决跨文件的函数引用问题。例如,stack.c被编译成stack.o,test.c被编译成test.o
3️⃣汇编:汇编代码➡️二进制机器码
4️⃣链接:合并、有些地方要用函数名去其他文件找函数地址
一旦所有的源文件被编译成目标文件,链接器(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中的函数定义,从而生成一个完整的可执行文件,所有的函数调用都被正确地解析和连接,这个地址修正的过程也叫做重定位
-
-
五、函数重载
C语言不允许同名函数
C++允许同名函数。要求:函数名相同,参数不同,构成 函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似但数据类型不同的问题。
代码示例:
#include<iostream>using namespace std;// 1、参数类型不同
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}
double Add(double left, double right)
{cout << "double Add(double left, double right)" << endl;return left + right;
}// 2、参数个数不同
void f()
{cout << "f()" << endl;
}
void f(int a)
{cout << "f(int a)" << endl;
}// 3、参数类型顺序不同
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;
}
int main()
{Add(10, 20);Add(10.1, 20.2);f();f(10);f(10, 'a');f('a', 10);return 0;
}
C语言不支持重载 链接时,直接用函数名去找地址,有同名函数的情况则区分不开。
-
C++是如何支持函数重载的?
通过函数名修饰实现的,只要函数参数不同,函数名就会被修饰成不同。然后直接用修饰好的名字,去找该函数的地址。
-
函数名修饰
名字修饰是编译器自动进行的一种处理过程,它将C++源代码中的函数名和变量名转换成包含更多信息的唯一标识符。这些信息通常包括函数的参数类型、参数数量等,甚至可能包括所属的类名(对于类成员函数),通过这种方式,每个重载的函数都会被赋予一个独一无二的名字,确保链接器在最后链接程序的时候能够区分它们
-
Linux下g++的修饰规则简单易懂,下面我们使 用了g++演示了这个修饰后的名字。 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度 +函数名+类型首字母】。
-
采用C语言编译器编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
-
采用C++编译器编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
通过以上这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修 饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。