C语⾔和C++的区别
c语⾔虽说经常和c++在⼀起被⼤家提起,但可千万不要以为它们是⼀个东西。现在我们常⽤的C语⾔是C89标准,C++是C++99标准的。C89就是在1989年制定的标准,如今最新的是C11和C++11标准。根据不同的标准,它们的功能也会有所不同,但是越新的版本⽀持的编译器越少,所以本⽂在讨论的时候使⽤的C语⾔标准是C89,C++标准是C++99.我们来介绍C语⾔和C++中那些不同的地⽅。
在C++中我们在定义或声明⼀个函数的时候,有时会在形参中给它赋⼀个初始值作为不传参数时候的缺省值,例如:
int FUN(int a = 10);
代表没有传参调⽤的时候,⾃动给a赋⼀个10的初始值。然⽽这种操作在c89下是⾏不通的,在c语⾔下这么写就会报错。
我们都知道,系统在调⽤任何⼀个函数的时候都有函数栈帧的开辟,如果函数有参数则需要压⼊实参。平常在我们⼈为给定实参的时候,是按照参数列表从右向左依次将参数通过
mov eax/ecx dword ptr[ebp-4] //假设是int数据
指令传⼊寄存器,再通过push指令压⼊。现在我们已经给定了函数参数的默认值,那么在压实参的时候只需要⼀步push初始值即可。效率更⾼。
另外需要注意的是,赋初始值必须从参数列表的右边开始赋值,从左边开始赋值将会出错:
int sum1(int a = 10,int b); //错误
int sum2(int a,int b = 20); //正确
因为如果sum1的声明是正确的,那么我们调⽤的时候怎么调⽤?sum1( ,20)?很可惜这样属于语法错误,调⽤这么写既然不对那就当然不能这样赋初始值了。相反,sum2的调⽤:sum2(20);合情合理,没有任何问题。
实际在写⼯程的时候,我们都习惯将函数的声明写在头⽂件中⽽⾮本⽂件⾥,然后在不同的⽂件中写出它们的定义。那么这种情况可以赋初始值吗?当然可以,不论是定义还是声明处,只要你遵守从右向左赋的规则就可以。甚⾄你还可以这样给初始值:
int fun(int a ,int b = 10);
int fun(int a = 20,int b);
眼尖的同学看见了下⾯的那⾏代码⼤喊错误,因为先给左边赋值了!
其实这样声明完全没有问题,两句声明是同⼀个函数(函数多次声明没有问题),第⼀句已经给b了⼀个初始值,运⾏到第⼆句时已经等价于int fun(int a = 20,int b = 10);了。但是注意,这两句的顺序不能反转,否则就是错误的。
总结:C89标准的C语⾔不⽀持函数默认值,C++⽀持函数默认值,且需要遵循从右向左赋初始值。
说到内联函数⼤家应当不陌⽣,它⼜是⼀个C89标准下C语⾔没有的函数。它的具体做法和宏⾮常相似,也是在调⽤处直接将代码展开,只不过宏它是在预编译阶段展开,⽽内联函数是在 编译阶段进⾏处理的。同时,宏作为预处理并不进⾏类型检查,⽽inline函数是要进⾏类型检查的,也就可以称作“更安全的宏”。
内联函数和普通函数的区别:内联函数没有栈帧的开辟回退,⼀般我们直接把内联函数写在头⽂件中,include之后就可以使⽤,由于调⽤时直接代码展开所以我们根本不需要担⼼什么重定义的问题——它连符号都没有⽣成当然不会所谓重定义了。普通函数⽣成符号,内联函数不会⽣成符号。
关于inline还需要注意的⼀点是,我们在使⽤它的时候往往是⽤来替换函数体⾮常⼩(1~5⾏代码)的函数的。这种情况下函数的堆栈开销相对函数体⼤⼩来说就⾮常⼤了,这种情况使⽤内联函数可以⼤⼤提⾼效率。相反如果是⼀个需要很多代码才能实现的函数,则不适合使⽤。⼀是此时函数堆栈调⽤开销与函数体相⽐已经是微不⾜道了,⼆是⼤量的代码直接展开的话会给调试带来很⼤的不便。三是如果代码体达到⼀个阈值,编译器会将它变成普通函数。
同时,递归函数不能声明为inline函数。说到底inline只是对编译器的建议,最终能否成功也不⼀定。同时,我们平常⽣成的都是debug版本,在这个版本下inline是不起作⽤的。只有⽣成release版时才会起作⽤。
总结:C89没有,在调⽤点直接展开,不⽣成符号,没有栈帧的开辟回退,仅在Release版本下⽣效。⼀般写在头⽂件中。
C语⾔中产⽣函数符号的规则是根据名称产⽣,这也就注定了c语⾔不存在函数重载的概念。⽽C++⽣成函数符号则考虑了函数名、参数个数、参数类型。需要注意的是函数的返回值并不能作为函数重载的依据,也就是说int sum和double sum这两个函数是不能构成重载的!
我们的函数重载也属于多态的⼀种,这就是所谓的静多态。
静多态:函数重载,函数模板
动多态(运⾏时的多态):继承中的多态(虚函数)。
使⽤重载的时候需要注意作作⽤域问题:请看如下代码。
#include <iostream>
using namespace std;
bool compare(int a,int b)
{
return a > b;
}
bool compare(double a,double b)
{
return a > b;
}
int main()
{
//bool compare(int a,int b);
compare(10,20);
compare(10.5,20.5);
return 0;
}
我在全局作⽤域定义了两个函数,它们由于参数类型不同可以构成重载,此时main函数中调⽤则可以正确的调⽤到各⾃的函数。
但是请看main函数中被注释掉的⼀句代码。如果我将它放出来,则会提出警告:将double类型转换成int类型可能会丢失数据。
这就意味着我们编译器针对下⾯两句调⽤都调⽤了参数类型int的compare。由此可见,编译器调⽤函数时优先在局部作⽤域搜索,若搜索成功则全部按照该函数的标准调⽤。若未搜索到才在全局作⽤域进⾏搜索。
总结:C语⾔不存在函数重载,C++根据函数名参数个数参数类型判断重载,属于静多态,必须同⼀作⽤域下才叫重载。
这⼀部分⾮常重要。在我的另⼀篇博客“”中对C语⾔中的const也有所讲解。当中提到了这么⼀个问题:C语⾔中被const修饰的变量不是常量,叫做常变量或者只读变量,这个常变量是⽆法当作数组下标的。然⽽在C++中const修饰的变量可以当作数组下标使⽤,成为了真正的常量。这就是C++对const的扩展。
C语⾔中的const:被修饰后不能做左值,可以不初始化,但是之后没有机会再初始化。不可以当数组的下标,可以通过指针修改。简单来说,它和普通变量的区别只是不能做左值⽽已。其他地⽅都是⼀
样的。
C++中的const:真正的常量。定义的时候必须初始化,可以⽤作数组的下标。const在C++中的编译规则是替换(和宏很像),所以它被看作是真正的常量。也可以通过指针修改。需要注意的是,C++的指针有可能退化成C语⾔的指针。⽐如以下情况:
int b = 20;
const int a = b;
这时候的a就只是⼀个普通的C语⾔的const常变量了,已经⽆法当数组的下标了。(引⽤了⼀个编译阶段不确定的值)
const在⽣成符号时,是local符号。即在本⽂件中才可见。如果⾮要在别的⽂件中使⽤它的话,在⽂件头部声明:extern cosnt int data = 10;这样⽣成的符号就是global符号。
总结:C中的const叫只读变量,只是⽆法做左值的变量;C++中的const是真正的常量,但也有可能退化成c语⾔的常量,默认⽣成local符号。
说到引⽤,我们第⼀反应就是想到了他的兄弟:指针。引⽤从底层来说和指针就是同⼀个东西,但是在编译器中它的特性和指针完全不同。
int a = 10;
int &b = a;
int *p = &a;
//b = 20;
//*p = 20;
⾸先定义⼀个变量a = 10,然后我们分别定义⼀个引⽤b以及⼀个指针p指向a。我们来转到反汇编看看底层的实现:
可以看到底层实现完全⼀致,取a的地址放⼊eax寄存器,再将eax中的值存⼊引⽤b/指针p的内存中。⾄此我们可以说(在底层)引⽤本质就是⼀个指针。
了解了底层实现,我们回到编译器。我们看到对a的值的修改,指针p的做法是*p = 20;即进⾏解引⽤后替换值。底层实现:
再来看看引⽤修改:
我们看到修改a的值的⽅法也是⼀样的,也是解引⽤。只是我们在调⽤的时候有所不同:调⽤p时需要*p解引⽤,b则直接使⽤就可以。由此我们 推断出:引⽤在直接使⽤时是指针解引⽤。p直接使⽤则是它⾃⼰的地址。
这样我们也了解了,我们给引⽤开辟的这块内存是根本访问不到的。如果直接⽤就直接解引⽤了。即使打印&b,输出的也是a的地址。
在此附上将指针转为引⽤的⼩技巧:int *p = &a,我们将 引⽤符号移到左边 将 *替换即可:int &p = a。
接下来看看如何创建数组的引⽤:
int array[10] = {0}; //定义⼀个数组
我们知道,array拿出来使⽤的话就是数组array的⾸元素地址。即是int *类型。
那么&array是什么意思呢?int **类型,⽤来指向array[0]地址的⼀个地址吗?不要想当然了,&array是整个数组类型。
那么要定义⼀个数组引⽤,按照上⾯的⼩诀窍,先来写写数组指针吧:
int (*q) [10] = &array;
将右侧的&对左边的*进⾏覆盖:
int (&q)[10] = array;
测试sizeof(q) = 10。我们成功创建了数组引⽤。
经过上⾯的详解 ,我们知道了引⽤其实就是取地址。那么我们都知道⼀个⽴即数是没有地址的,即
int &b = 10;
这样的代码是⽆法通过编译的。那如果你就是⾮要引⽤⼀个⽴即数,其实也不是没有办法:
const int &b = 10;
即将这个⽴即数⽤const修饰⼀下,就可以了。为什么呢?
这时因为被const修饰的都会产⽣⼀个临时量来保存这个数据,⾃然就有地址可取了。
总结:引⽤底层就是指针,使⽤时会直接解引⽤,可以配合const对⼀个⽴即数进⾏引⽤。
这个问题很有意思,也是重点需要关注的问题。malloc()和free()是C语⾔中动态申请内存和释放内存的标准库中的函数。⽽new和delete 是C++运算符、关键字。new和delete底层其实还是调⽤了malloc和free。它们之间的区别有以下⼏个⽅⾯:
①:malloc和free是函数,new和delete是运算符。
②:malloc在分配内存前需要⼤⼩,new不需要。
例如:int *p1 = (int *)malloc(sizeof(int));
int *p2 = new int; //int *p3 = new int(10);
malloc时需要指定⼤⼩,还需要类型转换。new时不需要指定⼤⼩因为它可以从给出的类型判断,并且还可以同时赋初始值。
③:malloc不安全,需要⼿动类型转换,new不需要类型转换。
详见上⼀条。
④:free只释放空间,delete先调⽤析构函数再释放空间(如果需要)。
递归函数c语言规则与第⑤条对应,如果使⽤了复杂类型,先析构再call operator delete回收内存。
⑤:new是先调⽤构造函数再申请空间(如果需要)。
与第④条对应,我们在调⽤new的时候(例如int *p2 = new int;这句代码 ),底层代码的实现是:⾸先push 4字节(int类型的⼤⼩),随后call operator new函数分配了内存。由于我们这句代码并未涉及到复杂类型(如类类型),所以也就没有构造函数的调⽤。如下是operator new的源代码,也是new实现的重要函数:
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{ // try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{ // report no memory
_THROW_NCEE(_XSTD bad_alloc, );
}
return (p);
}
我们可以看到,⾸先malloc(size)申请参数字节⼤⼩的内存,如果失败(malloc失败返回0)则进⼊判断:如果_callnewh(size)也失败的话,抛出bad_alloc异常。_callnewh()这个函数是在查看new handler是否可⽤,如果可⽤会释放⼀部分内存再返回到malloc处继续申请,如果new handler不可⽤就会抛出异常。
⑥:内存不⾜(开辟失败)时处理⽅式不同。
malloc失败返回0,new失败抛出bad_alloc异常。
⑦:new和malloc开辟内存的位置不同。
malloc开辟在堆区,new开辟在⾃由存储区域。
⑧:new可以调⽤malloc(),但malloc不能调⽤new。
new就是⽤malloc()实现的,new是C++独有malloc当然⽆法调⽤。
C语⾔中作⽤域只有两个:局部,全局。C++中则是有:局部作⽤域,类作⽤域,名字空间作⽤域三种。
所谓名字空间就是namespace,我们定义⼀个名字空间就是定义⼀个新作⽤域。访问时需要以如下⽅式访问(以std为例)
std::cin<< "123" <<std::endl;
例如我们有⼀个名字空间叫Myname,其中有⼀个变量叫做data。如果我们希望在其他地⽅使⽤data的话,需要在⽂件头声明:using Myname::data;这样⼀来data就使⽤的是Myname中的值了。可是这样每个符号我们都得声明岂不是累死?
我们只要using namespace Myname;就可以将其中所有符号导⼊了。
这也就是我们经常看到的using namespace std;的意思啦。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论