8.高质量编程
8.1.基础知识
1,应用 ifndef/define/endif结构产生预处理块的目的是:防止头文件被重复引用。
2,头文件中只存放声明,而不存放定义,注意:C 语法中,类的成员函数可以再声明的同时被定义,并且自动成为内联函数,这虽然会带来书写上的方便,但却造成了风格不一致,建议将成员函数的定义与声明分开,不论该函数体有多么小。
3,不提倡使用全局变量,尽量不要再头文件中出现 exter int value这类声明。
4,一行代码只做一件事情,如只定义一个变量,或只写一条语句。
5,尽可能在定义变量的同时初始化该变量。
6,if语句
(1)不可将布尔变量直接与true,false或者1,0进行比较
根据布尔类型的语义,零值为"假" (false),任何非零值都是“真”(true),true的值究竟是什么并没有统一的标准。
代码语言:javascript复制//bool flag
if (flag)//真
if (!flag)//假
//不良风格
if (flag == true)
if (flag == 0)
(2)应当将整型变量用 == 或 != 直接与 0 比较
代码语言:javascript复制//int value
if (value == 0)
//不良风格 误解是bool
if (!value)
(3)不可将浮点变量用 == 或 != 与任何数字比较
无论是float还是double类型的变量,都有精度限制,所以一定要避免将浮点类型变量用 == 与数字比较,应该设法转换成 >= 或 <= 形式
代码语言:javascript复制//float x
if (x >= erp)
//不良风格
if (x == 0.0)
(4)指针变量直接与 NULL比较,而不是与 0比较
尽管NULL的值与0相同,但是两者的意义不同,回答 if (NULL == p) 和 if (p == NULL)的区别?
因为NULL不能被赋值,如果漏写成 NULL = p,编译器会直接报错的,但是 如果漏写成 p = NULL,不会报错,if语句的含义就变味道了!
7,for循环语句
(1)在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU跨切循环层的次数,效率会提高
代码语言:javascript复制//低效率
for (int i =0 ; i <100; i )
{
for (int j =0 ; j< 5; j )
{
sum = a[i][j];
}
}
//高效率 长循环在内层
for (int i =0 ; i <5; i )
{
for (int j =0 ; j< 100; j )
{
sum = a[i][j];
}
}
(2)如果循环体内存在逻辑判断,并且循次数很大,将逻辑判断移到循环体的外面。,如果在内部,就会多执行很多次逻辑判断,并且破坏了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。
代码语言:javascript复制//效率低
for (int i =0; i < N; i )
{
if(cond)
{
DO();
}
else
{
DONothing();
}
}
//高效率但是不简洁
if (cond)
{
for (int i =0 ;i < N ; i )
{
DO();
}
}
else
{
for (int i =0 ;i < N ; i )
{
DONothing();
}
}
(3)不可再for循环体内修改循环变量,防止 for循环失去控制。
(4)建议for语句的循环控制变量的取值采用 半开闭区间的写法
代码语言:javascript复制//半开半闭
for (int i = 0; i < N;i )
//闭区间 不建议
for (int i = 0 ; i <= N -1; i )
8,goto语句不建议用,当不是禁止用,goto语句至少有一处可显神通,它能从多重循环体中一下跳到外面,不用写很多次 break语句。
代码语言:javascript复制{
{
{
goto error;
}
}
}
9,const常量有数据类型,而#define没有数据类型,编译器可以对前者进行类型安全检查,而只对后者进行字符替换,没有类型安全检查。const常量完全可以取代宏常量。
(1)不能再类声明中初始化 const数据成员,因为类的对象未被创建时,编译器不知道 SIZE的值是多少。
代码语言:javascript复制class A
{
//错误,在类声明中初始化const数据成员
const int SIZE = 100;
//错误,未知的SIZE
int array[SIZE];
};
(2)const 数据成员的初始化只能在类构造函数的初始化表中进行。
代码语言:javascript复制class A
{
A(int size);
const int SIZE;
};
A::A(int size):SIZE(size)
{
}
A a(200);
A b(100);
(3)通过类中的枚举常量实现在整个类中都恒定的常量,不用指望const数据成员了。
枚举常量不会占用对象的存储空间,它们在编译时被全部求值,其缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
代码语言:javascript复制class A
{
enum{SIZE =100, SIZE2 =200};
int array1[SIZE];
int arrray2[SIZE2];
};
10,函数设计
(1)如果函数没有参数,则用 void填充。
代码语言:javascript复制float GetValue(void);//良好风格
float GetValue();//不良风格
(2)目的参数放在前面,源参数放在后面
(3)如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。
代码语言:javascript复制void StringCopy(char *Des, const char *str)
(4)如果输入的参数以值传递的方式传递对象,则改用 const & 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
(5)如果函数的返回值是一个对象,有些场合用引用传递替换值传递可以提高效率,而有些场合只能用值传递而不能用引用传递,否则会出错。
代码语言:javascript复制class STring
{
//赋值函数
String & operate==(const STring &other);
//相加函数,如果没有 friend修饰只允许有一个右侧参数
friend String operate (const STring &s1, const String &s2);
private:
char *m_data;
};
//赋值函数的实现
String & String::operate=(const String &other)
{
if (this == &other)
{
return *this;
}
m_Data = new char[strlen(other.data) 1];
strcpy(m_data, other.data);
//返回的是 *this的引用,无需拷贝过程
retrun *this;
}
赋值函数,应用引用传递的方式返回String 对象,如果用值传递的方式,虽然功能仍然正确,但由于 retuern 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。
代码语言:javascript复制String a,b,c;
//如果用值传递,将产生一次 *this的拷贝
a =b;
//将产生两次的 *this拷贝
a=b=c;
String 的相加函数 operate 的实现如下。
代码语言:javascript复制String operate (const String &s1, const String &s2)
{
String temp;//值传递
delete temp.data;
temp.data = new char[strlen(s1.data) strlen(s2.data) 1];
strcpy(temp.data,s1.data);
strcat(temp.data,s2.data);
return temp;
}
对于相加函数,应用值传递的方式返回String对象,如果改用 引用传递,那么函数返回值是一个指向局部对象 temp的引用,由于temp在函数结束时被自动销毁,将导致返回的引用无效。
代码语言:javascript复制c =a b;//此时a b并不返回期望值,c什么也得不到,留下隐患
(6)return语句不可返回指向 栈内存 的指针或者引用,因为该内存在函数体结束时被自动销毁
代码语言:javascript复制char *FUN(void)
{
//str的内存位于栈上
char str[] = “hello"'
//将导致错误
return str;
}
如果函数返回值是一个对象,要考虑return语句的效率。创建一个临时对象并返回它,如下。
return String(s1 s2);
编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的消耗,提高了效率
区别:先创建一个局部对象 temp并返回它的结果
String temp(s1 s2);
return temp;
上述代码将发生三件事:
1,首先temp对象被创建,同时完成初始化
2,然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中
3,最后,temp在函数结束时被销毁,调用析构函数
类似地:
return int(x y)
不要写成:
int temp =x y;
return temp;
由于内部数据类型如 int,float ,double的变量不存在构造函数和析构函数,虽然该临时变量的语法不会提高多少效率,但是程序更加简洁易读。
(7)assert不是函数,而是宏
(8)引用与指针的区别
1,int m; int &n = m
n是m的一个引用,m是被引用物,n相当于m的别名,对n的任何操作就是对m的操作。所以n即不是m的拷贝,也不是指向m的指针,其实n就是m它自己。
2,引用被创建的同时必须被初始化,指针则可以在任何时候被初始化
3,不能有NULL引用,引用必须与合法的存储单元关联,指针则可以是NULL
4, 一旦引用被初始化,就不能改变引用的关系,指针则可以随时改变所指的对象
int i =5; int j =6; int &k = i; k = j;
k和i的值都变成了6。
8.2.内存管理
8.2.1.内存的分配方式
1,静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量,static变量
2,在栈上创建:在执行函数时候,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3,堆内存:动态内存分配,程序在运行时候用malloc或new申请任意多少的内存,程序员自己负责在何时用 free或delete释放内存,动态内存的生存期由我们决定,使用灵活,但是问题也多。
8.2.2.内存使用错误
1,内存分配未成功,却使用了它
使用之前检查是否存在,如果是函数入口,可以 assert(p != NULL),如果是malloc或new申请内存,可以 if (NULL == p)进行防错处理
2,内存分配虽然成功,但是尚未初始化就引用它
注意内存的缺省值初值是什么并没有统一标准,不一定全为0,因此,在创建数组时候,别忘记赋初始值,赋0也不能省略。
3,内存分配成功并且已经初始化,但操作越过了内存的边界
数组操作越界
4,忘记了释放内存,造成内存泄漏
含有这种错误的函数每被调用一次就丢失一块内存,刚开始时系统的内存充足,你看不到错误,终有一次程序突然死掉,系统出现提示:内存耗尽
malloc/free,new/delete必须成对出现
5,释放了内存却继续使用它
(1)return语句写错了,注意不要返回指向 栈内存 的指针或者引用,因为该内存在函数体结束时被自动销毁
(2)free或delete释放了内存之后,没有将指针设置为NULL,导致产生了 野指针
8.2.3.指针与数组对比
数组要么在静态存储区被创建,如全局数组,要么在栈上被创建。数组名对应着一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是 可变,所以我们常用指针来操作动态内存,指针远比数组灵活,但也更危险。
字符串比较指针与数组
1,修改内容
代码语言:javascript复制//字符数组a的容量是 6个字符,内容 hello
char a[] = "hello";
//a的内容可以改变
a[0] = 'X';
//指针p指向常量字符串 "world",位于静态存储区,内容是 world
char *p = "world";
//常量字符串的内容是不可以被修改的
p[0] = 'X';//运行时出错,编译器不能发现该错误
2,内容复制与比较
不能对数组名进行直接复制与比较,否则会产生编译错误;
指针 p =a 并不能把 a的内容复制指针 p,而是把 a的地址赋给了p。要想复制 a的内容,可以先用库函数 malloc为p申请一块容量 为 strlen(a) 1个字符的内存,再用strcpy进行字符串复制。
代码语言:javascript复制//数组
char a[] = "hello";
char b[10];
//不能 b = a,而是
strcpy(b,a);
//不能 b ==a ,而是
if(strcmp(b,a) == 0)
//指针
int len =strlen(a)
char *p = (char*)malloc(sizeof(char)*(len 1));
//不能 p = a,而是
strcpy(p,a);
//不能 p ==a ,而是
if(strcmp(p,a) == 0)
3,计算内存容量
sizeof计算数组的容量(字节数),并忘记 "