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

【C/C++高质量编程 笔记】

2011-05-26 22:56 281 查看
1.C语言标准的本质:标准C语言没有提供实现,只是定义了标准的函数接口,所有工作都是通过库函数完成的。

2.什么是语言实现:

具体实现一种语言的各种特征并支持特定编程模式的技术和工具,具体说就是编译器和连接器或者是解释器。

3. 基于应用程序框架(比如MFC),生成的源代码往往没有main(),并不是说这些程序不需要main函数,而只是Application Framework将main的实现隐藏起来了,并且它的实现具有固定的模式,所以不需要程序员来编写,在应用程序的连接阶段,框架会将包含main() 实现的Library加进来一起连接。

4.内部名称:C/C++都会按照特定的规则把程序员定义的标识符转换为相应的内部名称——在前边 添加下划线 " _ ",在C语言中,所有函数不是局部于编译单元(文件作用域)的static函数,就是具有extern连接类型和global作用域的全局函数,从唯一识 别函数上看并没有大的不同,但在C++中,允许用户在不同的作用域定义同名的函数,作用域不单单是文件,可能是class namespace等,甚至在同一作用域中也可以定义同名的函数——重载。在源码级别,通过它们各自的对象和成员标识符区分,但是在连接器层面,所有函数
都是全局函数,能够用来区分不同函数调用的除了作用域外就是函数名称了,C++中使用Name-Mangling避免连接二义性。

5.变量初始化需要注意的事项:

a. 在C++/C中,全局变量(extern或static的)存放在程序的静态数据区中,在程序进入main()之前创建,在main()结束后销毁,因此 在代码中根本没有机会初始化它们,语言及其实现提供了一个默认的全局初始化器0——没有明确初始化全局变量则将0转换为所需类型来完成初始化。函数内 的static局部变量和类的static数据成员都具有static存储类型,因此最终也被移动到程序的静态数据区中,因此也会默认初始化为0.除非你明确的提供了初值。

b.在一个编译单元中定义的全局变量的初始值不要依赖定义于另一个编译单元中的全局变量的初始值,因为编译器和连接器无法确定两个编译单元连接在一起时哪一个全局变量的初始化优先于另一个编译单元的全局变量的初始化。

c.存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。这两种变量都是保持变量内容持久性的方法。它们默认初始化都为0。

6.区别编译时和运行时的不同

编译时指的是编译预处理器、编译器和连接器工作的阶段,只对基本的规范和语法进行处理,而像容器越界访问、虚函数动态决议、函数动态连接、动态内存分配等需要在运行时才能确定的问题是运行时。

下边这个例子就说明了C++的访问控制策略是为了防止意外时间而不是防止对编译器的恶意欺骗。

/*

* =====================================================================================

*

* Filename: test.cpp

*

* Description: 区分编译时和运行时的不同

*

* Version: 1.0

* Created: 02/26/2010 09:24:06 AM

* Revision: none

* Compiler: gcc

*

* Author: gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com

* Company: IBM CDL

*

* =====================================================================================

*/

#include <iostream>

using namespace std;

class Base

{

public:

virtual void Say(){

cout<<"Base::Say()was invoked!/n";

}

}; /* ----- end of class Base ----- */

class Derived:public Base{

private:

virtual void Say(){

cout << "Derived::Say() was invoked!/n";

}

};

int main(int argc, char *argv[])

{

Base *p = new Derived;

p->Say();

}

输入:Derived::Say() was invoked! 这违背了private 本来的意愿。

7.字节是内存编制的最小单位,所以最小的对象(包括空对象)也至少会占据一个字节的内存空间。一个bool变量也占据了1字节内存,只是浪费了

8.void 是空类型,意思是这种类型的大小无法确定,不存在void类型的对象,void指针可以作为通用指针,因为它可以指向任何类型的对象。void类型指针和 NULL指针的区别,NULL是可以赋值给任何类型的指针的值0,在C中它为(void *)0,而在C++中,由于允许从0到任何指针类型的隐式转化,NULL就是整数0.一个Void *类型的指针是一个合法的指针,常用于函数参数来传递一个函数与其调用者约定好类型的对象地址,如线程函数。在C中允许任何非void类型指针和void 类型的指针之间进行直接的相互转化,但是在C++中只允许任何类型的指针向void类型的指针转化,而不允许反过来将void类型的指针直接指派给任何非
void类型指针,除非进行强制转换。这样就避免了内存扩张和截断的安全问题。

9.默认类型:C中为int,C++没有默认类型,但是在模板中有“默认类型参数”的概念。

10.高低地址存放:

所 谓自然对齐,基本数据类型(主要是short、int和double)的变量不能简单的存储在内存中的任意地址处,它们的起始地址必须能够被它们的大小整 除。RiSC的都是Big Endian存储,即高字节高字在低地址存放,要求自然对齐。而Intel的都是Little Endian,即高字节高字在高地址存放,不要求自然对齐。

11.类型转换:

一般占用内存比较少的类型会隐式的转换为表达式中占 用内存最多的操作数类型,类型转换并不是改变原来的类型和值,而是生成了新的临时变元:char is-a int, int is-a long, long is-a float, lfoat is-a double.

从内存的角度,一个类型转换过的指针所能够访问的范围受到其类型的限制,

例如,这实际上就是内存的截断,因为int指针能访问的范围小于Double型:

double d1= 1000.25100212;

*pInt =(int*)(&d1);

cout << *pInt <<endl;

注意,这里要区分值的截断和内存的截断,下边是值的截断:

double d2 = 10.20;

int i2 = (int)d2;

cout << i2 << endl;

而下边的这个例子就是内存的扩张,因为double指针能访问的范围大于int型:

int i1 = 1023;

double *pDouble = (double*)(&i1);

cout << *pDouble <<endl;

同理,在OO中,不能把基类的对象,直接转换为派生类对象,无论是直接赋值还是强制转换,因为这不是“自然的”。

12.++ --的效率问题:当单独使用时前置后置都一样,而当复杂的表达式中使用时,比如当应用于用户定义类型,尤其是大队想的时候,前置版本会比后置版本效率高许 多,原因是后置版本,比如b=a++, 其实质并非某些教科上所写的“先使用其操作数的值,然后再进行加1运算”,而是首先创建一个临时变量temp存储a的值,然后做a+=1的运算,随后把 temp的值赋给b,最后销毁这个临时变量(若是对象则还会调用其拷贝构造函数),所有这些是有代价的。所以在可以选择的情况下,尽量使用前置版本。下边 就写一个重载++运算符的例子:

/*

* =====================================================================================

*

* Filename: test.cpp

*

* Description: 重载++运算符

*

* Version: 1.0

* Created: 02/26/2010 09:24:06 AM

* Revision: none

* Compiler: gcc

*

* Author: gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com

* Company: IBM CDL

*

* =====================================================================================

*/

#include <iostream>

using namespace std;

class Integer{

public:

Integer(double data):m_data(data){}

Integer& operator++(){

cout << "前置版本,返回引用" <<endl;

m_data++;

return *this;

}

Integer operator++(int){

cout << "后置版本,返回对象的值" <<endl;

Integer temp = *this;

m_data++;

return temp;

}

int getData(){

return m_data;

}

private:

double m_data;

};

int main(int argc, char *argv[])

{

Integer x=1;

++x;

cout <<x.getData() <<endl;

x++;

cout <<x.getData() <<endl;

}

13.bool类型:c++中0->false,而任何非0值为true,所以应该总是和false比较。

14.不建议使用==和!=来比较浮点数是否相等(用abs比较),但是可以直接比较浮点数谁大谁小。

15.遍历数组的效率:

对于多维数组而言,高效率的遍历方法是看语言以什么顺序来安排数组元素的存储空间,我们看看c/c++是用什么方式存储的:

/*

* =====================================================================================

*

* Filename: test.cpp

*

* Description: C/C++多维数组存储是以先行后列的方式存储的,所以遍历时外循环是行,内循环是列效率较高

*

* Version: 1.0

* Created: 02/26/2010 09:24:06 AM

* Revision: none

* Compiler: gcc

*

* Author: gnuhpc (http://www.gnuhpc.info), warmbupt@gmail.com

* Company: IBM CDL

*

* =====================================================================================

*/

#include <iostream>

using namespace std;

int main(int argc, char *argv[])

{

int a[5][5];

for( int i=0 ; i<5 ; i++ )

{

for( int j=0 ; j<5 ; j++ )

{

cout << "a[" <<i <<"][" <<j <<"]=" << &a[i][j] <<" ";

}

cout <<endl;

}

}

影响效率的实际上是大数组遍历时来回跳转导致的内存页面交换次数以及cache命中率的高低,而不是循环次数本身。

16.循环体内存在逻辑判断,并且循环次数很大时,最好将逻辑判断移到循环体外,虽然看起来很罗嗦,但是编译器可以对循环进行优化处理:



17.字面常量,比如char c ='a', 只能引用不能修改,其保存在程序的符号表中而不是一般的数据区,为只读。在编译时通常把合并常量的开关打开可优化程序效率。

18. 在标准的C语言中,const符合常量默认是extern的,也就是说你不能在两个或以上的编译单元中同时定义一个同名的const符号常量,或者把一个 const符号常量定义放在一个头文件中而多个编译单元同时包含该文件。但是在标准的C++中,const为内连接,可以定义在头文件中。当在不同的编译 单元中同时包含该文件时,编译器认为它们是不同的符号常量,因为每个编译单元独立编译时分别为它们分配空间,连接时进行合并。

19.标准C++/C中的枚举常量的值可以很大,比如300000000000000000

20.在C++中应该尽可能使用const定义符号常量。

21.C++需要对外公开的常量放在头文件中,不需要对外公开的常量定义在文件的头部。为便于管理可以把不同模块的常量集中存放在一个公用的头文件中。

22.const 定义的常量在函数执行之后其空间会被释放,而 static 定义的静态常量在函数执行后不会被释放其空间。

static 表示的是静态的。类的静态成员函数,成员变量是和类相关的,不是和类的具体对象相关,即使没有具体的对象,也能调用类的静态成员函数,成员变量。一般的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。

在 c++ 中, static 静态成员变量不能在类内部初始化。
在 c++ 中, const 常量成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。

const 数据成员只在 某个对象生存期内是常量 , 而对于整个类而言却是可变的 。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类声明中初始化 const 数据成员,因为类的对象未被创建时,编译器不知道const

数据成员的值是什么。

const 数据成员的初始化 只能在类的构造函数的初始化表中进行 。 要想建立在整个类中都恒定的常量 ,应该用类中的枚举常量来实现,或者 static

const。

如:

class

Test

{

public:

Test():

a(0){}

enum {size1=100, size2 = 200 };

private:

const

int a; //

只能在构造函数初始化列表中初始化

static

int b ;

const

static int c; // 与 static const int

c; 相同,可以 在这里定义 (如果以后在类中需要使用该变量的话 ).

}

int Test :: b = 0;

// 不能以成员列表初始化,不能在定义处初始化,因为不属于某个对象。

const

int Test:: c = 0 ; // 注意:给静态成员变量赋值时,不在需要加 static 修饰。但 const 要加。

在这转载一篇写的比较清晰的文字:

全局变量/常量几种方法的区别(C/C++)

在讨论全局变量之前我们先要明白几个基本的概念:

1. 编译单元(模块):

在IDE开发工具大行其道的今天,对于编译的一些概念很多人已经不再清楚了,很多程序员最怕的就是处理连接错误(LINK ERROR),

因为它不像编译错误那样可以给出你程序错误的具体位置,你常常对这种错误感到懊恼,但是如果你经常使用gcc,makefile等工具在linux或者嵌

入式下做开发工作的话,那么你可能非常的理解编译与连接的区别!当在VC这样的开发工具上编写完代码,点击编译按钮准备生成exe文件时,VC其实做了两

步工作,第一步,将每个.cpp(.c)和相应.h文件编译成obj文件;第二步,将工程中所有的obj文件进行LINK生成最终的.exe文件,那么错

误就有可能在两个地方产生,一个是编译时的错误,这个主要是语法错误,另一个是连接错误,主要是重复定义变量等。我们所说的编译单元就是指在编译阶段生成

的每个obj文件,一个obj文件就是一个编译单元,也就是说一个cpp(.c)和它相应的.h文件共同组成了一个编译单元,一个工程由很多个编译单元组

成,每个obj文件里包含了变量存储的相对地址等 。

2. 声明与定义的区别

函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可以保证你的程序编译通过,

但是当函数或变量定义的时候,它就在内存中有了实际的物理空间,如果你在编译模块中引用的外部变量没有在整个工程中任何一个地方定义的话,

那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量!你也可以这样理解,

对同一个变量或函数的声明可以有多次,而定义只能有一次!

3. extern的作用

extern有两个作用,第一个,当它与"C"一起连用时,如: extern "C" void

fun(int a, int b); 则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,

C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$ 也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样),为什么这么做呢,因为C++支持函数的重载啊,在这里不去过多的论述这个问题,如果你有兴趣可以去网上搜索,相信你可以得到满意的解释!

当extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int;

它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块活其他模块中使用,记住它是一个声明不是定义!也就是说B模块(编译

单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,

在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

如果你对以上几个概念已经非常明白的话,那么让我们一起来看以下几种全局变量/常量的使用区别:

1. 用extern修饰的全局变量

以上已经说了extern的作用,下面我们来举个例子,如:

在test1.h中有下列声明:

#ifndef TEST1H

#define TEST1H

extern char g_str[]; // 声明全局变量g_str

void fun1();

#endif

在test1.cpp中

#include "test1.h"

char g_str[] = "123456"; // 定义全局变量g_str

void fun1()

{

cout << g_str << endl;

}

以上是test1模块, 它的编译和连接都可以通过,如果我们还有test2模块也想使用g_str,只需要在原文件中引用就可以了

#include "test1.h"

void fun2()

{

cout << g_str << endl;

}

以上test1和test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面着"123456"这

个字符串,但是你却不能在test2.obj里面找到,这是因为g_str是整个工程的全局变量,在内存中只存在一份,

test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!

有些人喜欢把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面test1.h改为

extern char g_str[] = "123456"; // 这个时候相当于没有extern

然后把test1.cpp中的g_str的定义去掉,这个时候再编译连接test1和test2两个模块时,会报连接错误,这是因为你把全局变量g_str的定义放在了头文件之后,test1.cpp这个模块包含了test1.h所以定义了一次g_str,而test2.cpp也包含了test1.h所以再一次定义了g_str,这个时候连接器在连接test1和test2时发现两个g_str。

如果你非要把g_str的定义放在test1.h中的话,那么就把test2的代码中#include "test1.h"去掉 换成:

extern char g_str[];

void fun2()

{

cout << g_str << endl;

}

这个时候编译器就知道g_str是引自于外部的一个编译模块了,不会在本模块中再重复定义一个出来,但是我想说这样做非常糟糕,因为你由于无法在test2.cpp中使用#include "test1.h",那么test1.h中声明的其他函数你也无法使用了,除非也用都用extern修饰,这样的话你光声明的函数就要一大串,而且头文件的作用就是要给外部提供接口使用的,所以 请记住, 只在头文件中做声明,真理总是这么简单。

2. 用static修饰的全局变量

首 先,我要告诉你static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量;其次,static修饰 的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;最后,static修饰全局变量的作用域只 能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它 。利用这一特性可以在不同的文件中定义 同名函数和同名变量,而不必担心命名冲突。如:

test1.h:

#ifndef TEST1H

#define TEST1H

static char g_str[] = "123456";

void fun1();

#endif

test1.cpp:

#include "test1.h"

void fun1()

{

cout << g_str << endl;

}

test2.cpp

#include "test1.h"

void fun2()

{

cout << g_str << endl;

}

以上两个编译单元可以连接成功, 当你打开test1.obj时,你可以在它里面找到字符串"123456",

同时你也可以在test2.obj中找到它们,它们之所以可以连接成功而没有报重复定义的错误是因为虽然它们有相同的内容,但是存储的物理地址并不一样,就像是两个不同变量赋了相同的值一样,而这两个变量分别作用于它们各自的编译单元。

也许你比较较真,自己偷偷的跟踪调试上面的代码,结果你发现两个编译单元(test1,test2)的g_str的内存地址相同,于是你下结论static修饰的变量也可以作用于其他模块,但是我要告诉你,那是你的编译器在欺骗你,大多数编

译器都对代码都有优化功能,以达到生成的目标程序更节省内存,执行效率更高,当编译器在连接各个编译单元的时候,它会把相同内容的内存只拷贝一份,比如上面的"123456", 位于两个编译单元中的变量都是同样的内容,那么在连接的时候它在内存中就只会存在一份了,如果你把上面的代码改成下面的样子,你马上就可以拆穿编译器的谎言:

test1.cpp:

#include "test1.h"

void fun1()

{

g_str[0] = ''a'';

cout << g_str << endl;

}

test2.cpp

#include "test1.h"

void fun2()

{

cout << g_str << endl;

}

void main()

{

fun1(); // a23456

fun2(); // 123456

}

这个时候你在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用。

正是因为static有以上的特性,所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染,同样记住这个原则吧!

3 const修饰的全局常量

const修饰的全局常量用途很广,比如软件中的错误信息字符串都是用全局常量来定义的。const修饰的全局常量据有跟static相同的特性,即它们只能作用于本编译模块中,但是const可以与extern连用来声明该常量可以作用于其他编译模块中 ,

extern const char g_str[];

然后在原文件中别忘了定义:

const char g_str[] = "123456";


所以当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!所以对const我没有什么可以过多的描述,我只是想提醒你,const char* g_str = "123456" 与 const char g_str[] =

"123465"是不同的, 前面那个const 修饰的是char *而不是g_str,它的g_str并不是常量,它被看做是一个定义了的全局变量(可以被其他编译单元使用)
, 所以如果你像让char*g_str遵守const的全局常量的规则,最好这么定义const char* const g_str="123456".

比较常用的在多个文件的工程中定义全局常量的方法:

方法1:在某个公用头文件中将符号常量定义为static(c++有无static无所谓),并初始化,例如:

//CommDef.h

static const int MAX=1024;

然后每一个使用它的编译单元包含该头文件即可。

方法2:在某个公用的头文件中将符号常量声明 为extern,例如

//CommDef.h

extern const int MAX;

并且在某一个源文件中定义一次:

const int MAX=1024;

然 后每一个使用它的编译单元包含上述头文件即可。

方法1的优点是维护方便,但是由于每一个符号常量在每一个包含了它们的编译单元内都存在一份独立的拷贝,若修改常量的初值则将影响到多个编译单元而导致必须重新编译,而且浪费空间。

方法2的优点是节约存储、编译后修改再编译节省时间,但维护比较不便。

23.C++/C语言,要取得一个变量或对象的内存地址的通用方法是:强制转换为void*,然后输出。

24.若输入参数以值传递的方式传递对象,则宜改用"const &"方式来传递,因为引用的创建和销毁不会调用对象的构造和析构函数,从而可提高效率。若函数的返回值是一个对象,有些场合可以使用“返回引用”替换“返回对象值”。而有时只能返回对象值。

25.不要将正常值和错误标志混在一起返回,建议正常值用输出参数获得,而错误标志用return语句返回,另外一种方法是将正常情况下的返回值和错误标志绑定成一个键值对<value,bool>,例如std::map的insert()方法。

26.有时候函数原本不需要返回值,但是为了增加灵活性,如支持链式表达可以附加返回值,比如strcpy。

27.标准C语言有4种存储类型,即:extern ,auto , static , register ,分为永久生存——extern和static,以及临时生存期限——auto和register。一个变量或函数只能有一种存储类型。

28. 连接类型有三种:外、内、无,表明了一个标识符的可见性,所以常常和作用域的概念混淆。所谓外连接,就是这个标识符可以在其他编译单元中或者在定义它的编 译单元中的其他范围内被调用。它需要在运行时分配空间。所外内连接指一个标识符能在定义它的编译单元中的其他范围内被调用,但是不能在其他的编译单元中被 调用。无连接指的是只能在声明它的范围内被调用。

29.assert不是函数而是宏,是一个完全无害的测试手段。 断言出错是程序员的错误,比如说程序员误传进了一个NULL指针,传进去了一个NULL的窗口句柄,或者编写不当,而不是程序使用者(用

户)的操作错误。在发行版本(Release)中,可以定义NDEBUG宏来取消所有断言。所以,断言不能够完全代替参数检查。

30. const 只能修饰函数的输入参数,输入参数若是使用“指针传递”则使用const进行保护,例如void Stringcpy(char *strDest, const char *strSrc),若是也想保护指针本身则可以在指针前加const:void Stringcpy(char *strDest, const char* const strSrc)。若是“值传递”,但是多次使用到传递进来的初值,则也可以加上const,保证代码不会无意修改它。而定义诸如void Func1(A
a)这样的函数一定是效率比较低的,因为函数体内将产生A类型的临时变量来拷贝a,而临时变量的构造、拷贝和析构都有const,所以可以使用传引用—— 只借助参数的别名,本质上是传递地址,此时需要加上const来进行保护:void Vunc1(const A &a),对于基本数据类型,这样的操作完全是没有必要的。另外,若是给返回指针的函数返回值前加上const,则返回值是一个契约性常量,不能被 直接修改,返回值只能被赋值给有const修饰的同类型指针。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: