您的位置:首页 > 编程语言 > C语言/C++

《高质量程序设计指南C/C++语言》笔记总结

2015-07-23 23:18 441 查看
在对林锐,韩永泉编著的《高质量程序设计指南C/C++语言》的学习中,我从中了解到了很多编程的小细节和重要的概念,特总结规整如下:

1.标准C语言允许任何非void类型的指针和void类型的指针之间进行直接的相互转换。但在C++中,可以把任何类型的指针直接指派给void类型指针,因为void*是一种通用指针;但是不能反过来将void类型指针直接指派给任何非void类型的指针,除非进行强制转换。因此在C语言环境中我们就可以先把一种具体类型的指针如int*转换为void*类型,然后再把void*类型转换为double*类型,而编译器不会认为这是错误的。然而这种做法确实存在着不易察觉的安全问题(内存扩张和截断等),这是标准C语言的一个缺陷。

2.如果计数器从0开始计数,则建议for语句的循环控制变量的取值采用“前闭后开区间”写法,要防止出现“差1”错误。

3.对于多维数组来说,正确的遍历方法要看语言以什么顺序来安排数组元素的存储空间。比如FORTRAN是以“先列后行”的顺序在内存中连续存放数组元素,而C/C++则是以“先行后列”的顺序来连续存储数组元素。因此,遍历方式与存储方式相同。根据事实证明:“先列后行”遍历发生的页面交换次数要比“先行后列”多,且cache命中率相对也低。这恰恰就是导致“先列后行”遍历效率降低的原因。

4.常用常量可分为:字面常量、符号常量、契约性常量、布尔常量和枚举常量等。

(1).字面常量:例如直接出现的各种进制的数字、字符(‘’括住的单个字符)或字符串(“”括住的一系列字符)等。其只能引用不能修改,所以语言实现一般将它保存在程序的符号表里而不是一般的数据区中。符号表是“只读”的,其实它是一种访问保护的机制,千万不要理解为只读存储器(ROM)。除了字符串外,你无法取一个字面常量的地址;

(2).符号常量:存在两种符号常量(用#define定义的宏常量和用const定义的常量),宏常量在本质上就是字面常量。在C语言中,用const定义的常量其实是值不能修改的常量,因此会给它分配存储空间(外连接的);但在C++中,const定义的常量要具体情况具体对待:对于基本数据类型的常量,,编译器会把它放在符号表中而不分配存储空间,而ADT/UDT的const对象则需要分配存储空间(大对象)。从理论上讲,只要你手中拥有一个对象的指针(内存地址),你就可以设法绕过编译器随意修改它的内容,除非该内存受到操作系统的保护。也就是说,C++并没有提供对指针有效性的静态检查,而是把它丢给了操作系统,这正是使用指针的危险所在。在标准C语言中,const符号常量默认是外连接的(分配内存),也就是说你不能两个(或两个以上)编译单元中同时定义一个同名的const符号常量(重复定义错误),或者把一个const符号常量定义放在一个头文件中而在多个编译单元中同时包含该头文件。但是在标准C++中,const符号常量默认是内连接的,因此可以定义在头文件中。当在不同的编译单元中同时包含该头文件时,编译器认为它们是不同的符号常量,因此每个编译单元独立编译时会分别为它们分配存储空间,而在连接时进行常量合并。

(3).契约性常量:契约性const对象的定义并未使用const关键字,但被看做是一个const对象,例如:

void ReadValue(const int& num)

{

cout<<num;

}

int main(void)

{

int n = 0;

ReadValue(n);//契约性const,n被看做是const

}

(4).枚举常量:C++/C的构造类型enum实际上常用来定义一些相关常量的集合,标准C++/C规定枚举常量的值是可以扩展的,并非受限于一般的整数型的范围。

5.函数调用规范(调用约定),其决定了函数调用的实参压栈、退栈及堆栈释放的方式,以及函数名改编的方案,也即命名方案。Windows环境下常用的调用规范有:

(1)._cdecl:这是C++/C函数的默认调用规范,参数从右向左依次传递并压入堆栈,由调用函数负责堆栈的清退,因此这种方式利于传递个数可变的参数给被调用函数(因为只有调用函数才知道它给被调用函数传递多少个参数及它们的类型)。

(2)._stdcall:这是Win API函数使用的调用规范。参数从右向左依次传递并压入堆栈,由被调用函数负责堆栈的清退。该规范生成的函数代码比_cdecl更小,但当函数有可变个数的参数是会转为_cdecl规范。在Windows中,宏WINAPI、CALLBACK都定义为_stdcall;

(3)._thiscall:是C++非静态成员函数的默认调用规范,不能使用个数可变的参数。当调用非静态成员函数的时候,this指针直接保存在ECX寄存器中而非压入函数堆栈。其他方面与_stdcall相同;

(4)._fastcall:该规范所修饰的函数的实参将被直接传递到CPU寄存器中而不是内存堆栈中(这就是“快速调用”的含义)。堆栈清退由被调用函数负责。该规则不能用于成员函数。

注意:类的静态成员函数的默认调用规范不是thiscall,类的友元函数的调用规范也不是thiscall,它们都是由函数本身指定或者由工程设定的。特别地,COM接口的方法都指定_stdcall调用规范,而我们自己开发COM对象及其接口时也可以指定其他调用规范。

6.如果函数的返回值是一个对象,有些场合下可以用“返回引用”替换“返回对象值”,这样可以提高效率,而且还可以支持链式表达。而有些场合下只能用“返回对象值”而不能用“返回引用”,否则会出错,例如:

char String

{

……

//赋值函数

String& operator = (const String &assign);

//相加函数,如果作为成员函数来重载,则只能有一个参数

friend String operator + (const String &lh ,const String &rh);

private:

char *m_data;

}

对于赋值函数,应当用“返回引用”的方式返回String对象(即this对象)。

String& operator = (const String &assign)

{

if(this == &assign)

{

return *this;

}

char *p = new char[strlen(assign.m_data)+1];

strcpy(p,assign.m_data);

delete m_data;

m_data = p;

return *this;//返回的是*this的引用,没有内容拷贝的过程

}

对于相加函数,应当用“返回对象值”的方式返回String对象,这将把局部对象temp及其真正的字符串值拷贝一份给调用环境接受者。如果改用“返回引用”,那么函数返回值是一个指向局部对象temp的“引用”(即地址),而temp在函数结束时被自动销毁,将导致返回的“引用”无效。

String operator + (const String &lh ,const String &rh)

{

String temp;

temp.m_data = new char[strlen(s1.m_data)+strlen(s2.m_data)+1];

strcpy(temp.m_data,s1.m_data);

strcat(temp.m_data,s2.m_data);

return temp;//执行string对象及其字符串内容的拷贝

}

7.在函数体的“入口处”和“出口处”从严把手,从而提高函数的质量。“入口处”:使用断言;“出口处”:对return语句的正确性和效率进行检查。

8.对于ADT/UDT的输入参数,应该将“值传递”改为“const&传递”,目的是提高效率(void Func(A a)改为void Func(const A &a));对于基本数据类型的输入参数就不需改变,否则即达不到提高效率也会让人费解(void Func(const int x)改为void Func(const int &x))。

9.数组实际上也是一个可以递归定义的概念:任何维数的数组都可以看作是比它少一维的数组组成的一维数组。例如int a[3][4][5]可以看做是由二维数组int b[4][5]组成的一维数组,其长度为3。

数组和指针之间存在等价关系:

(1).一维数组等价于元素的指针,例如:

int a[10] <==> int *const a;

(2).二维数组等价于指向一维数组的指针,例如:

int b[3][4] <==> int (*const b)[4];

(3).三维数组等价于指向二维数组的指针,例如:

int c[3][4][5] <==> int (*const c)[4][5];

10.引用和指针的比较:

(1).引用在创建的同时必须初始化,即引用到一个有效的对象;而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值;

(2).不存在NULL引用,引用必须与合法的存储单元关联;而指针则可以是NULL。注:不要用字面变量来初始化引用;

(3).引用一旦被初始化为指向一个对象,它就不能被改变为对另一对象的引用(即“从一而终、矢志不渝”);而指针在任何时候都可以改变为指向另一个对象。给引用赋值并不是改变它和原始对象的绑定关系;

(4).引用的创建和销毁并不会调用类的拷贝构造和析构函数;

(5).在语言层面,引用的用法和对象一样;在二进制层面,引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换。

注:引用的主要用途是修饰函数的形参和返回值,引用既具有质真的效率,又具有变量使用的方便性和直观性。

11.条件编译

(1).#if、 #elif、 #else、 #ifdef、 #ifndef;

(2).#error编译伪指令用于输出与平台、环境等有关信息;

(3).#pragma编译伪指令用于执行语言实现所定义的动作;

(4).#和##运算符

构串操作符#只能修饰带参数的宏的形参,它将实参的字符序列(而不是实参代表的值)转换成字符串常量。例如:

#define STRING(x) #x #x #x

#define TEXT(x) "class" #x "Info"

那么宏引用:

int abc = 100;

STRING(abc)

TEXT(abc)

展开后结果是:

“abcabcabc”

"classabcInfo"

合并操作符##将出现在其左右的的字符序列合并成一个新的标识符(注意:不是字符串)。例如:

#define CLASS_NAME(name) class##name

#define MERGE(x,y) x##y##x

则宏引用:

CLASS_NAME(SysTimer)

MERGE(me,To)

将分别扩展为如下两个标识符:

classSysTimer

meTome

使用合并操作符##时,产生的标示符必须预先有定义,否则编译器会报“标识符未定义”的编译错误。

12.在C++中,凡是使用static关键字声明和定义的程序元素,不论其作用域是文件、函数或是类,都将具有static存储类型,并且其生存期限为永久,即在程序开始运行时创建而在程序结束时销毁。

13.类String的构造函数、拷贝构造函数、拷贝赋值函数和析构函数

class String

{

public:

String(const char *str = "");//构造函数

String(const String& copy);//拷贝构造函数

String& operator = (const String& assign);//拷贝赋值函数

~String();//析构函数

private:

size_t m_size;//保存当前长度

char *m_data;//指向字符串的指针

}

String::String(const char *str = "")//构造函数
{
if(str == NULL)
{
m_data = new char[1];
*m_data = '\0';
m_size = 0;
}
else
{
size_t length = strlen(str);
m_data = new char[length+1];
strcpy(m_data,str);
m_size = length;
}
}

String::String(const String& copy)//拷贝构造函数
{
size_t len = strlen(copy.m_data);
m_data = new char[len+1];
strcpy(m_data,copy.m_data);
m_size = len;
}

String::String& operator = (const String& assign)//拷贝赋值函数
{
//检查自赋值
if(this != &assign)
{ //分配新的内存资源,并拷贝内容
char *temp = new char[strlen(assign.m_data)+1];
strcpy(temp,assign.m_data);
//释放原有的内存资源
delete []m_data;
m_data = temp;
m_size = strlen(assign.m_data);
}
//返回本对象引用
return *this;
}

String::~String()//析构函数
{
delete []m_data;
}
14.成员函数的重载、覆盖和隐藏
(1).成员函数被重载的特征:
a.具有相同的作用域(即同一个类定义中);
b.函数名字相同;
c.参数类型、顺序和数目不同(包括const参数和非const参数);
d.virtual关键字可有可无。
(2).成员函数被覆盖的特征:
a.不同的作用域(分别位于派生类和基类中);
b.函数名字相同;

c.参数列表完全相同;
d.基类函数必须是虚函数。
(3).成员函数被隐藏的特征:
a.派生类函数与基类的函数同名,但是参数列表有所差异。此时,无论有无virtual关键字,基类的函数在派生类中将被隐藏;
b.派生类的函数与基类的函数同名,参数列表也相同,但是基类函数没有virtual关键字。此时,基类的函数在派生类中被隐藏。

15.重载++

(1).前置++:int b = ++a; <==> a += 1; int b = a;

(2).后置++:Int b = a++; <==>int temp = a; a += 1; int b = temp; temp:~int();

16.static成员函数不能定义为const的,这是因为static成员函数只是全局函数的一个形式的封装,而全局函数不存在const一说;何况static成员函数不能访问类的非静态成员(没有this指针),修改非静态数据成员又从何说起?

17.new/delete的各种用法

new/deleteplain(普通new)nothrow(不抛出异常的new)placement(放置调用对象构造函数)
对象new type_name;new(nothrow) type_name;new(p) type_name;
delete p;delete p;delete p;
对象数组new type_name[x];new(nothrow) type_name[x];new(p) type_name[x];
delete []p;delete []p;delete []p;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: