好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
如果函数没有返回值,那么应声明为void类型
在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为void类型。例如:
add ( int a, int b )
{
return a + b;
}
int main(int argc, char* argv[])
{
printf ( "2 + 3 = %d", add ( 2, 3) );
}
//程序运行的结果为输出:2 + 3 = 5
这说明不加返回值说明的函数的确为int函数。
林锐博士《高质量C/C++编程》中提到:“C++语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。
因此,为了避免混乱,在编写C/C++程序时,对于任何函数都必须一个不漏地指定其类型。
如果函数没有返回值,一定要声明为void类型。这既是程序良好可读性的需要,也是编程规范性的要求。
另外,加上void类型声明后,也可以发挥代码的"自注释"作用。代码的“自注释”即代码能自己注释自己。
如果函数的参数可以是任意类型指针,那么应声明其参数为void *
。
在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
也就是引用另一个文件的全局变量或函数要用extern进行引用声明,
1. extern修饰变量的声明。
举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。
还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了。好像extern声明只能用于文件作用域似的。
2. extern修饰函数声明。
从本质上来讲,变量和函数没有区别,对于函数extern
可以省略。extern int fun(int mu)
可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。
使用extern和包含头文件来引用函数有什么区别呢?
extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这大概是KISS原则的一种体现吧!
这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。但坏处是函数的原型该了,你不得不改掉所有其他文件中的extern的声明。
3. extern “C”
extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
被extern "C"修饰的变量和函数是按照C语言方式编译和链接的
比如在C++中调用C库函数,就需要在C++程序中用extern “C”
声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
应用场合:
* 1.C++代码调用C语言代码、在C++的头文件中使用
* 2.如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C"{}。
4. 总结:
对变量而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的变量,方法有2种:
* (1)在A文件中必须用extern声明在B文件中定义的变量(当然是全局变量);
* (2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的变量声明,也即在这个头文件中必须用extern声明该变量。
对函数而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的函数,方法有2种:
* (1)在A文件中用extern声明在B文件中定义的函数(其实,也可省略extern,只需在A文件中出现B文件定义函数原型即可);
* (2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的函数原型,在头文件中函数可以不用加extern。
那是一个被遗忘的年代,那时,编译器只认识.c(或.cpp)文件,而不知道.h是何物的年代。
那时的人们写了很多的.c(或.cpp)文件,渐渐地,人们发现在很多.c文件中的声明变量或函数原型是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c文件。但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(或.cpp)文件,并修改其中的声明,啊~,简直是世界末日降临!
终于,有人再不能忍受这样的折磨,他(们)将重复的部分提取出来,放在一个新文件里,然后在需要的.c(或.cpp)文件中敲入#include XXXX这样的语句。这样即使某个声明发生了变更,也再不需要到处寻找与修改了。
因为这个新文件,经常被放在.c(或.cpp)文件的头部,所以就给它起名叫做“头文件”。
< >
引用的是编译器的类库路径里面的头文件
" "
引用的是你程序目录的相对路径中的头文件,在程序目录的相对路径中找不到该头文件时会继续在类库路径里搜寻该头文件。
C++标准程序库中的所有标识符都被定义于一个名为std
的namespace中
C++ 头文件的现状:
1) 旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中。
2) 新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间 std 中。
3) 标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中。
4) 具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,只是内容在 std 中。
不过现实情况和 C++ 标准所期望的有些不同,对于原来C语言的头文件,即使按照 C++ 的方式来使用,即#include <cstdio>
这种形式,那么符号可以位于命名空间 std 中,也可以位于全局范围中,大部分编译器在实现时并没有严格遵循C++标准,它们对带std和不带std的都支持
c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。
一 :<iostream>
和<iostream.h>
格式不一样,后缀为.h的头文件c++标准已经明确提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h
。 因此,
1)当使用<iostream.h>
时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
2)当使用<iostream>
的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
一个是为了兼容以前的C++代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加".h"
1、直接指定标识符。例如std::cout
2、使用using关键字。 using std::cout; using std::endl; using std::cin;
以上程序可以写成 cout << std::hex << 3.4 << endl
;
3、最方便的就是使用using namespace std
; 例如: using namespace std;这样命名空间std内定义的所有标识符都有效。
很多教程中都是这样做的,将 std 直接声明在所有函数外部,这样虽然使用方便,但在中大型项目开发中是不被推荐的,这样做增加了命名冲突的风险,我推荐在函数内部声明 std。
cout 和 cin 都是 C++ 的内置对象,而不是关键字。cout 和 cin 就分别是 ostream 和 istream 类的对象,只不过它们是由标准库的开发者提前创建好的,可以直接拿来使用。这种在 C++ 中提前创建好的对象称为内置对象。
推荐大家使用 cin、cout,它们比C语言中的 scanf、printf 更加灵活易用
C89 规定,所有局部变量都必须定义在函数开头,在定义好变量之前不能有其他的执行语句。C99 标准取消这这条限制,但是 VC/VS 对 C99 的支持很不积极,仍然要求变量定义在函数开头。而C++彻底取消了这个限制
#define
,是一个值替换的过程。void main()
{
const int a = 10; //c里面的const是一个冒牌货
//a = 11; 直接修改a不可以
int *p = (int *)&a;
*p = 11; //但是间接修改a可以 ,而在C++中却不可以
printf("a: %d \n", a);
system("pause");
}
在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,最明显的是可以自动调用构造函数和析构函数.
c11中增加了智能指针,可以用它来代替new和delete。
函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
由于内联函数比较短小,我们通常的做法是省略函数原型,将整个函数定义(包括函数头和函数体)放在本应该提供函数原型的地方。
宏定义一不小心就会踩坑,而且不一定在编译和运行时发现,给程序埋下隐患。
和宏一样,内联函数可以定义在头文件中(不用加 static 关键字),并且头文件被多次#include后也不会引发重复定义错误。这一点和非内联函数不同,非内联函数是禁止定义在头文件中的,它所在的头文件被多次#include
后会引发重复定义错误。
inline只是一种请求,编译器不一定允许这种请求。
内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误。这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏。
主要有两个作用:
* 一是消除函数调用时的开销
* 二是取代带参数的宏。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。
inline 关键字可以只在函数定义处添加,也可以只在函数声明处添加,也可以同时添加;但是在函数声明处添加 inline 关键字是无效的,编译器会忽略函数声明处的 inline 关键字。也就是说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
尽管大多数教科书中在函数声明和函数定义处都增加了 inline 关键字,但我认为 inline 关键字不应该出现在函数声明处。这个细节虽然不会影响函数的功能,但是体现了高质量 C++ 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
更为严格地说,内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格。
在多文件编程时,我建议将内联函数的定义直接放在头文件中,并且禁用内联函数的声明,因为将内联函数的声明和定义分散到不同的文件中会出错
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码。
C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。
在以后设计类时你将发现,通过使用默认参数,可以减少要定义的构造函数、方法以及方法重载的数量。
到底在声明中还是定义中指定默认参数?
放到头文件中就行
C++ 规定,在给定的作用域中只能指定一次默认参数
编译器使用的是当前作用域中的默认参数。站在编译器的角度看,它不管当前作用域中是函数声明还是函数定义,只要有默认参数就可以使用。
不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认参数
参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。
重载的结果是让一个函数名拥有了多种用途,使得命名更加方便,调用更加灵活
在使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人觉得莫名其妙。
C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_int,void Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。
不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。
从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
函数调用时编译器会根据传入的实参的个数、类型、顺序等信息去匹配要调用的函数,这在大部分情况下都能够精确匹配。但当实参的类型和形参的类型不一致时情况就会变得稍微复杂,例如函数形参的类型是int,调用函数时却将short类型的数据交给了它,编译器就需要先将short类型转换为int类型才能匹配成功。 但是类型转换会为重载决议造成困惑,不知道该确定哪一个函数,于是会造成编译错误。
#include <iostream>
using namespace std;
//1号函数
void func(char ch){
cout<<"#1"<<endl;
}
//3号函数
void func(long m){
cout<<"#3"<<endl;
}
//4号函数
void func(double f){
cout<<"#4"<<endl;
}
int main(){
short s = 99;
float f = 84.6;
func('a');
func(s);
func(49);
func(f);
return 0;
}
这段代码在编译时发生了错误,大概的意思是:func(s)和func(49)这两个函数发生调用错误,它们可以匹配三个重载函数中的任何一个,编译器不知道如何抉择。
在C语言中,重复定义多个同名的全局变量是合法的
在C++中,不允许定义多个同名的全局变量
C语言中多个同名的全局变量最终会被链接到全局数据区的同一个地址空间上
int g_var;
int g_var = 1;
C++直接拒绝这种二义性的做法。
register关键字请求“编译器”将局部变量存储于寄存器中,以加快其存储速度。
C语言中无法取得register变量地址
C++中可以取得register变量的地址 C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效。
C++编译器有自己的优化方式,不使用register也可能做优化
早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译器能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。
int main22()
{
register int a = 0;
printf("&a = %x\n", &a);
system("pause");
return 0;
}
C++在C语言的基本类型系统之上增加了bool
C++中的bool可取的值只有true和false
C++编译器会在赋值时将非0值转换为true,0值转换为false
C语言返回变量的值,C++语言是返回变量本身
C语言中的三目运算符返回的是变量值,不能做为左值使用,C++可以做左值
三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用
左值 能被放在 = 做值 称为左值,当左值的条件, 这段内存空间可以被写。
(a < b ? a : b ) = 30;// c++做的手脚 *((a < b ? &a : &b ))=30
(a < b ? 1 : b ) 有常量了就不能做左值了
C语言的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型
C++中的struct是一个新类型的定义声明
在C中定义一个结构体类型要用typedef:
typedef struct Student
{
int a;
}Stu;
于是在声明变量的时候就可:Stu stu1;(如果没有typedef就必须用struct Student stu1;来声明)