详解.c与.cpp 的区别
说起c和cpp的区别,我想大部分学计算机的都说不全,这次我就给大家好好总结一下。
一、带默认值的函数
带默认值的函数,顾名思义就是函数形参中带有默认值,比如下面这段c++代码,
1 |
|
#include |
它的函数形参a和被赋予了10和20,这段代码在cpp中完全正确,可以编译运行,而在c中是会编译出错的,原因就是c不支持函数带默认值,出现语法错误,如图:
那么函数默认值有什么规则呢?函数在定义或声明的时候,是从左往右给我们的形参赋予默认值的,当然,你也可以不赋予,比如有三个形参,可以只赋予其中的一个,接下来就是函数调用了,这个需要重点关注一下,函数调用是先压实参,从右向左入栈,栈底指针偏移,函数调用是自上而下的,以下面这段代码为例。
1 |
|
#include |
在cpp中,12、13、123组合用是完全正确的,因为函数调用是从右往左且自上而下的,1中参数末尾已经给出了默认值,c实参已入栈,下面函数默认c有实参;而2、3分开使用的话,就会报错,因为参数末尾没有默认值,实参是从右往左压栈的;但是14组合用的话就不对了,因为c被定义了两次,出现重定义错误,默认值只能给一次;那么,23、24能组合吗?答案是不行的,因为函数调用是自上而下的;1、2、3除了以上错误的组合外,其他组合都是正确的。
二、inline(内联)函数
内联函数在cpp里可以使用,而在c中不能使用,它相当于c中的宏,它的作用是在函数的调用点,把函数全展开,它的用法是在一个函数前加inline,就生成了内联函数,只在本文件可见。下面是它常考的知识点:
(1)它没有标准的函数栈帧的开辟和回退。
(2)它只在release版本中不会产生栈的开辟和回退,在debug版本中还是会产生。
(3)对于一些调用函数的开销 > 函数执行的开销的时,就用inline,但是要是调用还没有执行的开销大,就不需要使用。
(4)inline 写在头文件中,不写在源文件中。
inline与宏比较:
相同处:它们在函数中做一个展开替换的工作。
不同处:inline在编译期间处理,宏在预编译期间处理;inline还会做一个安全检查的工作,但是宏不做。
inline与普通函数比较
相同处:inline也是函数的一种。
不同处:inline函数没有标准的栈帧的开辟和回退,不产生符号,只本文件可见,但是普通函数不仅有出栈入栈的过程,也会产生符号。
4.inline与static函数
相同处:都是本文件文件可见。
不同处:inline函数没有标准的栈帧的开辟和回退,static函数有正常函数栈帧的开辟和回退,这一点和普通函数一样,不过static函数产生的符号都是local的,在链接中只处理global,不处理local的。
链接过程:符号解析-》符号分配内存虚拟地址-》符号重定位-》call-》pc
三、函数重载
何为函数重载,简单地说,函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。
函数重载需要解决两个问题:
第一个问题是声明定义重载函数时,是如何解决命名冲突的?
第二个问题是当我们调用一个重载函数时,编译器是如何解析的呢?
我们先来解决第一个问题。我们先来看一下下面这段代码:
1 |
|
bool compare(int a,int b)//compare_int_int |
虽然代码中,三个函数命名都是compare,但是并没有出现函数重定义这样的错误,并且可以正常运行,主要是因为它们虽然函数名相同,但是函数列表不同,完全符合函数重载规则,什么是函数重载规则呢?函数的重载的规则:
1、函数名称必须相同。
2、参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
3、函数的返回类型可以相同也可以不相同。
4、仅仅返回类型不同不足以成为函数的重载。
基于以上四点来解决命名冲突。
C++代码在编译时会根据参数列表对函数进行重命名,例如bool compare(int a, int b)会被重命名为_compare_int_int,bool compare(float x, float y)会被重命名为_compare_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。正是有重载决议,才有了函数重载。
接下来解决第二个问题。
编译器实现调用重载函数解析机制的时候,肯定是首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。下面介绍一种重载函数解析的方法:编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:
这个四个解析步骤所做的事情大致如下:
1.由匹配文法中的函数调用,获取函数名;
2.获得函数各参数表达式类型;
3.语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
4.语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。重载函数解析大致可以分为三步:
1.根据函数名确定候选函数集
2.从候选函数集中选择可用函数集合
3.从可用函数集中确定最佳函数,或由于模凌两可返回错误
1.1、根据函数名确定候选函数集
根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载。
2.2、确定可用函数
可用的函数是指:函数参数个数匹配并且每一个参数都有隐式转换序列。
(1)如果实参有m个参数,所有候选参数中,有且只有 m个参数;
(2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
(3)所有候选参数中,参数个数超过 m个,当前仅当第m + 1个参数以后都有缺省值。
满足以上,即为可用函数。
3.3、确定最佳匹配函数
确定可用函数之后,对可用函数集中的每一个函数,根据调用函数的实参,计算优先级,以优先级最高的匹配之。
四、const
在c语言里
1.const修饰的量叫做常变量,不是常量,不能做数组下标;
2.它和普通的变量的唯一区别是常变量定以后,不能做左值,而普通变量可以;
3.const修饰的量在整个工程可见,初不初始化都可以。
在c++里
1.const修饰的量就叫做常量,能做数组下标;
2.必须要初始化,初始化明确,是一个局部常量,比如const int a = 10,但是在前面加一个extern后,就变成全局的了,
3.当它引用一个编译阶段不明确的值时,就变成常变量了
五、引用
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名,如int a = 10;int &b = a。
c语言里没有引用,c++有引用,它相当于c语言里的指针,下面总结一下引用的知识点:
1. 引用必须初始化,初始化的值必须要能取地址,比如int a = 10;int &b = a;二int &b = 10是错误的,10是一个立即数,不能取地址。
2. 引用具有唯一性,不能改变,不能给一个变量重复引用,比如int &a = b;int &a = c;是错误的。
3. 访问引用变量永远访问的是它所引用的内存。
4. 引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
5. 引用不参与类型。
6. 引用比指针安全,因为引用它访问的是一块内存,不会存在野指针的问题。
7. 在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
8. 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
9. 引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
10. 使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
六、extern”c”
extern "C"是c++的解析,它的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
这个功能主要用在下面的情况:
1、C++代码调用C语言代码
2、C代码调用C++语言代码
3、在C++的头文件中使用
4、在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到
七、malloc和new
1、c语言申请动态内存和释放内存:malloc与new申请动态内存,free和delete释放内存。不过在C语言里常用malloc/free申请动态内存和释放内存。
比如double *pd=(double *) malloc (sizeof(double)*12);//分配12个double型存储单元,并将首地址存储到指针变量pd中。
详解:
内存块大小确定:malloc是通过我们计算然后得到一块新内存,然后指定数据类型并且内存值也是随机的。
使用时:需要引入头文件库函数 stdlib.h 或是 malloc.h(malloc.h与alloc.h一致)。
内存分配位置:堆中动态分配的内存。
具体分配过程:由程序向操作系统申请,操作系统遍历空闲结点链表,将第一个大于申请空间的堆结点分配给程序,然后将空闲结点链表中此节点删掉。
成功分配:返回值为指向被分配内存的指针。
失败分配:返回值为空NULL。
内存块释放:free()函数 将内存还给程序或操作系统。
注意:malloc与free都属于c/c++标准库函数,在使用时应该配对 申请之后不释放就会有可能发生内存泄漏。
使用free时需要检查指针是否为空。
2、c++语言申请动态内存和释放内存:new申请动态内存,delete释放内存。是c++独有的。
详解:
在使用:new时不止分配内存,还会进行初始化,执行相应构造函数,初始化时需要指定数据类型。
内存分配位置:自由存储区为对象分配内存。
使用时:无需引入头文件,new是保留字。
new 和 delete 是配对使用的。
在使用delete时注意将指针置为0,否则会形成悬垂指针(指针所指内存已被释放,仍指向该内存),造成错误。
new 可以看做是malloc 加 构造函数的执行,就是new更高级一些。
延伸:
malloc是库函数只能作用于内部数据类型,对于非内部数据动态对象而言,就不能完成对象的初始化与销毁,即执行构造函数与析构函数,而new 与 delete此类运算符就能够在编译器的控制权限内完成,对象的初始化与销毁任务,即执行构造函数与析构函数。
内存泄漏对于malloc或者new都可以检查出来的,区别在于new可以指明是那个文件的那一行,而malloc没有这些信息。