Cpp_C和C++区别
C++体系内容
要掌握C++,需要建立以下知识体系:
- 编译链接过程
- 函数调用过程
- C和C++区别
- 基础部分和C++基本一样,只需区分
- 数组、循环、判断、宏不再讲,一样
- 面向对象
- 运算符重载
- 模板
- 继承与多态
- STL:容器、泛型算法、函数对象
- 智能指针
- C++项目
本篇文章通过介绍C和C的区别,一窥C的体系形态。
输入输出
1 |
|
1 |
|
函数的区别
函数参数默认值
1 | int fun(int a, int b, int c) |
1 | int fun(int a, int b, int c = 10) |
1 | //以下情况不可以,在调用此函数时,会出现歧义。因此编译器不能通过。 |
1 | //但是如下代码可以运行,结果为1, 10, 20. |
1 | //错误,函数的默认值参数在同一作用域只能赋值一次,不能重复给相同的一个参数赋值。 |
1 | //不可行。编译是针对单文件的,所以不能知道在其他文件函数中定义的函数参数默认值 |
总结
因为函数参数的默认值是在编译器带入的,所以函数的参数的默认值只能在本单文件(不包括头文件,头文件的信息是在预编译期就展开了。)生效。
inline和内联
在调用内联函数时,该函数会在调用点将代码展开(复制)。
1 | inline int fun1(int a, int b) |
与一般函数的对比
-
一般函数调用是一个消耗很大的过程
- 传参
- call调用函数
- 开辟栈帧
- 如果有返回值则返回结果
- 栈帧回退
- 参数清除
-
内联函数
- 在函数的调用点展开(函数体中的指令复制过去)
- 不用传参
- 不用call
- 不用…
debug和release对比
- 在debug版本,内联函数失效,和正常函数调用方式一致;
- 在release版本,在调用内联函数时候,该函数会在调用点展开。
哪些函数不能被展开
内联函数是在编译时期,生成指令时展开的。
- 递归函数无法展开。因为递归函数的终止条件一定需要由变量决定,递归层数不定。而编译期是无法得知变量的具体值。所以递归函数不能被处理为内联函数。
inline和内联的关系
1 | inline int fun1(int a, int b) |
为何不会报错?
inline只是对编译器、CPU的建议,声明建议将该函数处理为内联。实际情况由编译器视情况决定。意思就是:虽然有的函数加了inline修饰,但是结果不一定被内联处理。
宏函数、static函数、内联函数的对比
宏函数
1 |
- 预编译时期在调用点展开
- 无法调试
- 没有参数类型安全校验,因为它连类型都无法指出
- 作用域:单文件可见
- 预编译期就展开了,不生成符号
static函数
- 不展开
- 可以调试
- 有参数类型安全校验
- 作用域:单文件可见
- 生成local型符号
内联函数
- debug版本不展开;release版本在调用点展开
- debug版本可以调试;release版本不可调试
- 有参数类型安全校验
- 作用域:单文件可见
- debug生成local符号;release版本不生产符号。
- 为何在release版本不生成符号?:编译期间已经展开了,相当于函数签名是透明的,不能生成符号
- 为何在debug版本生成符号?:debug要求能够调试函数,既然要调试函数,就要知道函数的详细信息,不能内联处理,所以要生成符号以标识。
- 为何debug版本生成的是local符号?:编译期只处理、生成单文件,内联函数本意是只展开在本单文件中,不能用于其他文件,所以只能是local符号。
普通函数
不展开,可以调试,有参数类型校验,多文件可见,生成global符号。
函数的重载-静多态
函数的重载–静多态–编译时期的多态–早绑定
函数的原型
函数的重载就要看准函数的原型:包括函数返回类型、函数名、形参列表(其中形参名可省略),且不需要函数体。
但是我们不能拿函数返回类型作为重载的标志,因为会产生二义性。
1 | char Max(char a, char b) |
所以Cpp中的重载就是:函数名相同,参数列表不同。
按函数生成的符号区分函数
符号不能同名,否则被认为是重定义。因此生成的符号不能一样。
Cpp中的函数可以重载,C语言不可以。因为C语言和Cpp生成函数符号的效果不同。
- C语言生成函数符号仅依赖函数名。
1 | //c语言的写法 |
- C++生成函数符号依赖函数名和参数列表(返回值不影响)。
1 | bool compare(int a, int b) |
静多态
函数重载后,究竟调用哪个函数?是在编译时期决定的,因为编译生成指令和符号,才能确定call哪个具体的函数——静多态的一种
1 | //可以运行,因为虽然main中只是声明,但是会生成UND符号,会寻找链接。 |
名字修饰约定(名字粉碎)
修饰名(Decoration name)
“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C”或“C++”函数等。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
名字修饰约定
名字修饰约定随调用约定和编译种类(C或C++文件)的不同而变化。下面分别说明。
- C编译时函数名修饰约定规则:
__stdcall
调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number
。__cdecl
调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname
。__fastcall
调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number
。
它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
- C++编译时函数名修饰约定规则:
__stdcall
调用约定:- 以“
?
”标识函数名的开始,后跟函数名; - 函数名后面以“
@@YG
”标识参数表的开始,后跟参数表; - 参数表以代号表示
X--void , D--char, E--unsigned char, F--short, H--int,I--unsigned int, J--long, K--unsigned long, M--float,N--double, _N--bool,....
PA
表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0
”代替,一个“0
”代表一次重复;
- 参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
- 参数表后以“
@Z
”标识整个名字的结束,如果该函数无参数,则以“Z
”标识结束。- 其格式为“
?functionname@@YA*****@Z
”或“?functionname@@YG*XZ
”。例如int Test1(char *var1,unsigned long)
:“?Test1@@YGHPADK@Z
”
- 其格式为“
- 以“
__cdecl
调用约定:规则同上面的_stdcall
调用约定,只是参数表的开始标识由上面的“@@YG
”变为“@@YA
”。__fastcall
调用约定:规则同上面的_stdcall
调用约定,只是参数表的开始标识由上面的“@@YG
”变为“@@YI
”- VC对函数的缺省声明是"
__cedcl
",将只能被C/C调用。
符号
符号=数据+指令
符号的来源
所有的数据都会生成符号。
指令中(比如函数声明)只有函数名会生成符号。
符号又可分为两种
-
全局符号-global符号
- 所有的文件都可以引入。
-
局部符号-local符号
- 只有本文件可见。
具体表现
- 普通的函数生成的是global符号。
- 被inline修饰的函数,语义为只在本文件可见,所以生成的是local符号。
- 如果函数只是声明而未在本文件定义,生成的符号是UND。
- inline函数在debug版本生成的是local型符号;如果处理为内联之后,在release版本下不生成符号,因为它已在调用点处展开了。
extern关键字
两种用法:
- C中,干预编译器,extern "C"是以C方式编译,extern "C"是以C++方式编译;
- C语言中,告诉编译器,函数是外部函数,既可以用在本文件,也可用在其他文件。
在cpp文件中调用c文件
矛盾点:
- C调用C–C产生函数符号-(函数名+参数类型列表),C语言产生函数符号-(函数名)
- C语言调用C+±-如果将C的函数符号改为C语言的函数符号–需要改动C源文件–不现实。正确解决办法是添加自己实现的C文件,写**C函数作为中间层去调用需要的C函数,然后让自实现的C函数产生C语言符号(extern C)**。
1 |
|
所以,如何在cpp文件中调用c函数?
1 | //如何解决——使用C语言的方式编译和生成符号。 |
如果想反过来呢?在c文件中怎么调用cpp的代码?如何解决?
1 |
|
解决方案:中间加一层
1 | //C调用C++的代码方法 |
namespace
实际上是个头文件。
1 | //tmp.h |
1 |
|
另外一种使用方式
1 | using AA::INT;//拿出命名空间中特定的某一个使用。 |
namespace主要作用是封装,防止命名冲突问题。
指针和数组
1 | //fun.cpp |
1 | using AA::INT; |
const
1 | //c_main.c |
C:常-变量
不能作为左值。
C++:常量
1 | int main() |
面试
为什么常量必须初始化?
因为如果要使用常量又不初始化的话,后期没有机会改。使用一个随机值对于程序没有意义。
如果使用变量给const修饰的量初始化,则该量会退化为常变量。
指针和const
1 | int main() |
要点1:const修饰的内容不能作为左值
要点2:不能泄露常量的地址给非常量的指针
const修饰的类型是离它最近的第一个成型的类型。其余的是它修饰的内容。
1 | int fun(int a) |
如果const修饰的内容不包含指针,则无法参与类型。
动态内存
C语言
malloc-free
1 | //使用malloc和free申请、释放一维数组、二位数组 |
C++
new-delete
1 | int main() |
1 | int main() |
new和malloc区别
- new是关键字,malloc是函数,new调用malloc。
- new和malloc都是在堆区申请空间
- new有三个步骤
- 开辟空间
- 构造函数。—初始化,这是与malloc的区别。
- 返回地址
- new不会强转返回值类型
- 封装计算sizeof
- 空间不足时,new会抛异常(bad_alloc)。malloc会返回空指针。(
int *p = new(nothrow) int(20);
)
new三种调用形式
- 关键字
- `int *p = new int(10);
- 函数
int *p = ::operator new(sizeof(int));
把new当函数调用,类似于malloc,没有初始化!仍需强转和传入sizeof字节数。但是还是会抛异常。operator delete(p);
相当于free,需要用函数形式delete释放,即operator delete(p)
- 与正常new区别就是没有初始化;
- 需要用函数形式delete释放,即operator delete(p)
- 定位new
1 | int *pa = (int*)::operator new(sizeof(int)); |
引用
是什么
张三——有个小名,二狗子。二狗子和张三是同一个人。
1 | int main() |
引用的底层是一个指针,
在使用到引用的地方,编译期会自动替换成指针的解引用。
1 | //c |
引用为什么必须初始化?
引用为什么一旦初始化就无法改变引用的方向?
1 | int main() |
笔试常见
1 | int fun1() |
注意点
- 定义时必须初始化;
- 没有空引用
- 没有二级引用
常引用
1 | int a=10; |
两个概念
面向过程
1 | int flag = 0; |
面向对象
1 | class Note |