第一篇 C/C++
写在前面
我把c和c++放在一起写,是因为虽然说c++不是c的扩展,但完全兼容c代码,也就是说使用c++的编译器可以编译c代码,甚至能发现c代码中一些难以发觉的错误。
在windows下头文件后缀为.h,c源文件后缀为.c,c++源文件后缀为.cpp;在unix/linux下c++的源文件后缀为.C(大写字符C)。
一 头文件、声名、定义
1. 头文件
作用:
将源原文件中的变量、函数、类等信息暴露给另外的源文件,供其使用。或提供库的调用接口。
编译器一次只处理一个源文件,这个源文件中若用到其他源文件中的变量、函数、类等,就得直接或间接的包含暴露这些信息的头文件,被包含的头文件和源文件一起编译。
为了便于项目的开发和管理,我们一般将程序的代码根据功能的不同写在不同的源文件中,但不同于java或.net,在c/c++中我们一般将变量、函数、类的声明放在头文件中,将定义和实现放在源文件中。
编写:
为了防止同一个头文件在嵌套包含的过程中被循环包含,在编写头文件的过程中必须使用ifndef/define/endif等结构进行预处理。
例如
#ifndef __MAIN_H__
#define __MAIN_H__
#include <stdio.h>
#endif
建议那个宏采用如下的命名格式:二个下划线+大写的头文件名(如果头文件名由多个单词组成,则每个单词大写,用一个下划线连接)+一个下划线+大写字母H+二个下划线。也有使用guid的(如mfc中)。
注意在变量、结构、类、函数的声明语句后加上”;”表示语句的结束,要不然编译器会提示很奇怪的错误或出现难以觉察运行时错误。刚开始的时候总容易忘记在结构和类的声明结尾使用”;”,可能是受到不需要在函数的定义结尾使用”;”的影响。
使用:
由于编译器搜索头文件路径的顺序不同,为了提高查找速度,所以对于标准库中的头文件,使用类似#include <stdio.h>的语句进行包含,对于本工程中的头文件,使用类似#include "HeadA.h"的语句进行包含。
在使用标准c++时,所包含的头文件没有.h后缀,例如#include <iostream>,但注意在使用的时候需要导入标准c++ stl库的命名空间,例如在源文件中使用using namespace std;。
对于c库中的头文件,在标准c++中的头文件命名方法为原头文件名去掉.h后缀,并在前面加上字符c,例如#include <cstdlib>。
相关问题:
在一个源文件中定义的数组不能在另一个源文件中使用sizeof获得大小。
原因是sizeof操作符只是在“编译时(compile time)”起作用,而C语言的编译单位是每次单个.c文件进行编译(其它语言也都如此)。因此,sizeof可以确定同一个源文件中某个数组的大小,但是对于定义在另一个源文件中的数组它无能为力了,因为那已经是“运行时(run time)”才能确定的事情了。
2. 声明
作用:
纯粹的声明说明的并非自身,而是告诉编译器,在被编译的文件中变量或函数等可以使用,只不过是在别的地方定义的。
使用:
声明可以重复出现多次,纯粹的声明一般用在头文件中,使用extern关键字,例如:extern int a;。在声明数组时不需要知道数组的大小,例如extern char buffer[];,因为编译器所需要的只是获得这个类型的变量的名字,知道他是在别的地方定义的。
对于函数的声明,可以省略extern关键字,例如:int MethodA();。
头文件可能被不止一个源文件包含,所以头文件中只能放声明,不要放定义,要不然在链接的时候会提示被定义的东西重复定义。
在c语言中,函数的使用要在声明之后,c++没有这个限制。
3. 定义
作用:
对于变量,定义相当于特殊的声明,定义确定变量的类型,并为其分配内存。对于函数,实现函数功能的代码,为代码段分配内存。
使用:
在同一作用域中,不能重复定义。在c语言中,变量的定义必须在{}包含的代码块的开始处,在其他非定义语句前,并且函数的调用要在此函数的声明之后。在c++中没有这些限制。
在源文件中,定义一个变量的同时也就进行了声明,同时为变量分配了存储空间。
int a; //声明并定义的变量a,同时分配了存储空间,一般的编译器会将a初始化为0。
int b = 5; //声明,定义并赋值。
对于数组,在定义时必须让编译器能知道数组的大小,不管是一维的还是多维的,因为要分配内存。
相关问题:
如果在定义时的类型是数组,那么在暴露它的头文件中进行声明时必须使用数组类型,而不能使用指针,尽管数组在作函数参数时会自动转换为指针。反之也是的,定义是指针类型,声明必须也是指针类型。具体原因可以参考《c专家编程》。
二 预处理、宏、类型自定义
1. 预处理
作用:
在进行编译前执行,对代码做文字上的处理。比如把其它文件包含到要编译的文件中、定义符号常量和宏,程序代码的条件编译、以及预处理指令的条件执行。在编写跨平台的代码时一般使用条件编译来编译平台相异部分的代码。
使用:
所有的预处理都以#开头,一行预处理指令前只能出现空格。
常用的预定义的符号常量有,__LINE__(当前源代码行的行号)、__FILE__(当前编译的文件的文件名)。
2. 宏
作用:
对代码做文字替换,提高代码的可读性和可维护性,使用宏可以起到函数的作用,而节省函数调用的系统开销,设计精巧的宏可以很优雅的完成复杂的功能。
使用:
#define 指令用来定义宏。
不带参数的宏用来定义符号常量或做文字替换,可以提高程序的可读性,不过在c++下最好使用const变量代替符号常量,因为符号常量只是做简单的文字替换,在调试的时候可能会带来麻烦。
带参数的宏在预处理阶段,先用替换文本取代参数,然后把宏展开。例如一个求绝对值的宏定义:
#define ABS(x)
(((x) > 0) ? (x) : -(x))
在编写windows动态链接库的时候,声名和定义要暴露的接口一般可以使用如下宏定义:
#define IMPORT_C __declspec(dllexport) //在头文件中使用,表示导入
#define EXPORT_C __declspec(dllexport) //在源文件中使用,表示导出
注意问题:
带参数的宏定义在展开的时候很容易因为参数的改变而违背宏原本的意思,所以要注意对参数使用(),即使使用(),错误也防不胜防,所以使用带参数的宏的时候尽量传简单的参数。
使用空格+反斜线(\)可以使宏定义写在多行上。
3. 类型定自定义
作用:
为一种数据类型定义一个新名字,这里的数据类型可以是内部数据类型(如int、char *)也可以是自定义的数据类型(struct、class)。主要是为变量的类型定义一个简单并且意义明确的类型名,简化复杂类型的变量的定义。
使用:
使用typedef关键字。
为了代码的跨平台而将不同平台下的内部数据类型定义为同一个类型名字(如
brew和symbian下的数据类型),这是最简单的typedef使用,如:
typedef uint16 AECHAR;
在结构体类型的定义中,比如我们定义一个整数链表的结点类型:
typedef struct _NODE
{
int data; //此结点表示的整数
struct _NODE *pNext; //指向下一个结点的指针,此处不能用PNODE *pNext;因为PNODE此时还没有被定义
}NODE, *PNODE;
这样我们在定义一个结点或结点指针的时候就可以这样写:
NODE node; //定义一个结点
PNODE pNode; //定义一个结点指针
如果不使用typedef,我们需要这样写:
struct _NODE
{
int data;
struct _NODE *PNext;
};
struct _NODE node; //注意struct关键字和_NODE一起才表示一个结构类型
struct _NODE *pNode;
c/c++编译器还支持typedef给一个还没有声明的类型启新名字,比如我们这样写:
typedef struct _NODE *PNODE; //此时struct _NODE还没声名呢
struct _NODE
{
int data;
PNODE pNext; //现在PNODE可以用了
};
PNODE *pNode;
这个特性还经常用来解决类之间由于相互依赖但头文件有不能相互包含的场合,当然,这种设计应该避免(头文件如果相互包含,成了个循环,编译肯定出错),比如:
//类A的头文件A.h
#include "B.h"
class A
{
public:
B *b; //对类B的依赖
};
//类B的头文件B.h
//#include "A.h" //必须注释掉这行,如果包含类A的头文件就造成相互包含了
typedef class A _A; //虽然在这里class A还没有声明,但是可以为它启新的名字,只是我们要在类B的源文件里包含类A的头文件
class B
{
public:
_A *a; //对类A的依赖,用class A的别名
};
//类B的源文件文件B.cpp
#include "B.h"
#include "A.h" //在源文件里包含类A的头文件
typedef也经常用来简化复杂类型的变量的定义,先看看以下定义:
int (*a[5])(int, char *); //定义了一个数组,数组中放的是函数指针,指向返回类型为int,参数为int, char *的函数
如果我们采用typedef就会清楚些:
typedef int (*pFN)(int, char *); //pFN为函数指针类型的别名
pFN a[5]; //定义一个数组,数据类型为函数指针
三 字符、字符串
1. 字符
char类型可以存放一个字符,在一般的机器上占一字节(byte),在我们使用的微机中,一个字节一般占8位(bit)。byte类型实际上是一个宏定义#define byte unsigned char;。在socket编程中,接收和发送数据的缓冲区一般用byte数组来定义。
在将char转换为无符号整数时使用:
char a = 'a';
unsigned int i = (unsigned char)a;
关于宽字符,参考《windows程序设计》,那里全。
2. 字符串
字符串"abcde"在内存中所占的字节数为实际字符所占的字节数加1(1个结束字符,即ascii的0)。字符数组和字符指针都可以用字符串常量来初始化,初始化指针时创建的字符串常量是只读的,修改其中的字符,程序会出现未知的错误。与指针相反,由字符串常量初始化的数组的值可以修改。
char a[] = "abcde"; //sizeof(a) = 6
char b[] = {'a', 'b', 'c', 'd', 'e'}; //sizeof(b) = 5
char *p = "abcde"; //sizeof(p) = 4
a[0] = 'g'; //ok
p[0] = 'g'; //access violation
对于字符数组的定义,最好使得最后一个字符为结束字符。
char b[] = {'a', 'b', 0};
最后一个数字0,表示结束字符的ascii码,这样在使用一些字符串操作函数时才不会出错。
四 类型转换
c的显式类型转为(type) expression,在c++中,引入了四个转换操作符:static_cast, const_cast, dynamic_cast, 和reinterpret_cast。
对于c风格的显示转换,可以用static_cast<type>(expression) 代替。
const_cast用于类型转换掉表达式的const或volatileness属性。
dynamic_cast用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。
reinterpret_cast 仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换。
参考资料:
http://www3.ccw.com.cn/club/essence/200103/617.htm
c/c++中的数字和字符串的转换没有java或c#中的方便,没有ToString()等方法,当然可以通过api来实现。对于字符串的格式化,sprintf函数提供了很强大的功能。
1. 数字类型(int、long、float、double等)转换为字符串(char *、char[]、string)
第一种方法是可以使用c库的itoa和ltoa函数来完成(不过不能转换浮点数),第二种方法就是使用sprintf函数,将数字按照指定的格式格式化后输出到指定的字符缓冲区(char *)。
string类型为标准c++库中的类型,提供丰富的构造函数,可以直接通过char *来构造。所以我们可以先将数字类型转为char *,然后再使用char *构造string对象。
下面的c代码,需要#include <stdlib.h>
void IntegerToString()
{
int i = 1234;
double d = 1234.0000;
long l = 123456;
char cI[100];
char cD[100];
char cL[100];
itoa(i, cI, 10); //十进制
sprintf(cD, "%f", d); //浮点数的转换
ltoa(l, cL, 2); //二进制
puts(cI);
puts(cD);
puts(cL);
}
对于c++的代码,使用标准c++库,需要#include <cstdlib>
string sD(cD); //通过char *构造string对象
cout << sD.data() << endl;
2. 字符串到数字类型
使用c库的atoi、atof、atol函数。
void StringToInteger()
{
float f;
char *cF = "1234.56";
f = atof(cF);
printf("%4.2f\n", f);
}
在c++中:
void StringToInteger()
{
int i;
float f;
string sI("456");
char *cF = "1234.56";
i = atoi(sI.data());
f = atof(cF);
printf("%d\n", i);
printf("%4.2f\n", f);
}
3. 将byte数组中的数据以16进制显示
在做socket通讯程序的时候,经常要对接收到的数据进行分析,一般是查看他们的16进制代码。
五 Const
这一节全是别人写的,忘记是在那里找的资料了,谢谢作者,讲的非常好。
关于C++中的const关键字的用法非常灵活,而使用const将大大改善程序的健壮性,现将本人的一些体会总结如下,期望对大家有所帮助:
1.const基础
如果const关键字不涉及到指针,我们很好理解,下面是涉及到指针的情况:
int b = 500;
const int* a = &b; //[1]
int const *a = &b; //[2]
int* const a = &b; //[3]
const int* const a = &b; //[4]
如果你能区分出上述四种情况,那么,恭喜你,你已经迈出了可喜的一步。不知道,也没关系,我们可以参考《Effective c++》Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对内容进行更改操作,如不能*a = 3 ;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。
另外const 的一些强大的功能在于它在函数声明中的应用。在一个函数声明中,const 可以修饰函数的返回值,或某个参数;对于成员函数,还可以修饰是整个函数。有如下几种情况,以下会逐渐的说明用法:
A& operator=(const A& a);
void fun0(const A* a );
void fun1( ) const; // fun1( ) 为类成员函数
const A fun2( );
2.const的初始化
先看一下const变量初始化的情况
1) 非指针const常量初始化的情况:
A b;
const A a = b;
2) 指针(引用)const常量初始化的情况:
A* d = new A();
const A* c = d;
或者:
const A* c = new A();
引用:
A f;
const A& e = f; // 这样作e只能访问声明为const的函数,而不能访问一般的成员函数;
[思考1]: 以下的这种赋值方法正确吗?
const A* c=new A();
A* e = c;
[思考2]: 以下的这种赋值方法正确吗?
A* const c = new A();
A* b = c;
3.作为参数和返回值的const修饰符
其实,不论是参数还是返回值,道理都是一样的,参数传入时候和函数返回的时候,初始化const变量
1)修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,如形参为const A* a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A& a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
[注意]:参数const通常用于参数为指针或引用的情况;
2)修饰返回值的const,如const A fun2( ); const A* fun3( );
这样声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
返回值用const修饰可以防止允许这样的操作发生:
Rational a,b;
Radional c;
(a*b) = c; //错误
一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
[总结]:一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。
原因如下:
如果返回值为某个对象为const(const A test = A 实例)或某个对象的引用为const(const A& test = A实例) ,则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。
[思考3]: 这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);
4.类成员函数中const的使用
一般放在函数体后,形如:void fun() const;
如果一个成员函数的不会修改数据成员,那么最好将其声明为const,因为const成员函数中不允许对数据成员进行修改,如果修改,编译器将报错,这大大提高了程序的健壮性。
5.使用const的一些建议
1)要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2)要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3)在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4)const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5)不要轻易的将函数的返回值类型定为const;
6 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
本人水平有限,欢迎批评指正,可以联系 kangjd@epri.ac.cn
[思考题答案]
1)这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
2)这种方法正确,因为声明指针所指向的内容可变;
3)这种做法不正确;
在const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
A a,b,c:
(a=b)=c;
因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量