分享
 
 
 

第一篇 C/C++ (2)

王朝c/c++·作者佚名  2006-01-31
窄屏简体版  字體: |||超大  

一 数组、指针、引用

数组是一种简单的数据结构,用来在一块连续的内存空间中存储多个相同类型的变量。数组名和数组第一个元素的地址都是这块内存空间的首地址,要访问数组中的元素可以使用”数组名[索引]”的形式,也可以使用”*(数组名+索引)”的形式。索引从0开始。比如:

int a[10] = {1, 2, 3, 4};

int c = (int)a;

int d = (int)&a[0];

int e = (int)&a;

int f = a[3]; //取第4个数字等于4

int g = *(a + 3); //a在这个时候已经退化为指针了,指向数组分配的内存的首地址,a + 3是将指针向后移动3个数组所存的变量所占的内存字节,本例中为3×4,12个字节,而不是3个字节。

c = d = e,f = g。

关于二维数组或多维数组,c/c++环境下在内存中是以行优先的顺序排列的,即一行一行的排列。比如:

int a[10][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

int c = (int)a;

int d = (int)&a[0];

int e = (int)&a[0][0];

int f = (int)&a;

int g = a[2][1];

int h = *(*(a + 2) + 1);

c = d = e = f。g = h。对于二维数组a[i][j],a[i]就是第i行所分配内存的首地址。

指针是一种变量,只是保存的是内存地址(变量所占内存的首地址,函数代码段的入口地址等)。要分清楚的就是指针所指向的地址和指针本身的地址(指针作为变量,要占内存,也就有地址)。一般的指针占4个字节,在c++中由于继承和虚拟的原因也可能出现超过4个字节的指针。

比如我们有如下定义:

int *pI = NULL;

我们说pI是个指针,也许是我们一听到指针就想到地址,而把pI是个变量这一点忽略了,其实pI就是个变量,这个变量的类型是指向int型数据的指针类型,变量的值是一个int型数据在内存中的地址。既然pI是个变量,那它也占内存,它所占内存的地址就是指针pI本身的地址,显然与pI的值所代表的地址是不一样的。

当指针指向数组或字符串或一块内存区域时,通过指针去获取数组中的数据或字符或内存中的某种类型的数据(这种类型也是指针指向的数据的类型)也可以采用二中方式,一种是”*(指针名 + 索引 )”,一种是”指针名[索引]”。索引从0开始。

char *pC = "ABCD";

char c = pC[2]; //取第3个字符'C'

int *pI = (int *)malloc(sizeof(int) * 4); //pI指向一块内存,这块内存用来存放int型数据

pI[0] = 1; //设置第一个int元素为1

*(pI + 1) = 2; //设置第二个int元素为2

free(pI);

需要记住的一点是在对指针做加操作时,地址是以指针指向的数据类型所占的内存字节为单位跳跃的。

int a[10];

char b[10];

int *pA = a + 3; //pA指向a后3×4=12个字节的地址,因为int一般占4个字节

char *pB = b + 3; //pB指向b后3×1=3个字节的地址,因为char一般占一个字节

如果想在函数中将对实参的改变持续到函数的作用域外,那就得传实参的地址,如果实参是指针,那就传指针的地址,也就是指针的指针。在c++下还也可以使用传引用来达到这样的目的,在讲引用的时候会讲。比如:

int a = 10;

int b = 20;

int *c = &a; //指针c指向a

int e = (int)&c;

ChangePointData(&c, &b); //想通过函数改变指针c的指向,需要传c的地址

int f = (int)&c;

void ChangePointData(int **p, int *data) //第一个参数类型为指针的地址,即指针的指针

{

*p = data;

}

我们通过函数改变指针c的指向,让c指向的b的地址,并让c在函数结束后仍然指向b的地址,那么传给函数的就必须是c的地址,形参类型也就是指针的指针类型。我们经常看到这样的代码,先在外部定义一个指针,然后通过一个函数在堆(关于堆和堆栈将在下一节讲)上分配一块内存,并让外部定义的那个指针指向这块内存,这个时候我们传给函数的必是外部这个指针的地址,因为我们要改变指针的指向。如果ChangePointData函数这样写:

int a = 10;

int b = 20;

int *c = &a;

int e = (int)&c;

ChangePointData(c, &b);

int f = (int)&c;

void ChangePointData(int *p, int *data)

{

p = data;

}

虽然在函数体内,看似改变了c的指向,实际上c的指向并没有变。这是因为c/c++的函数在调用过程中,实参是以值的形式传入函数的,也就是说函数操作的数据只是实参的一份拷贝,这个拷贝的值在函数的调用堆栈中,既然数据都不在同一块内存区域,函数的操作就根本不能影响实参了。并且函数结束,调用堆栈清空。为了达到修改的目的,我们只需要在函数体内获得要修改的变量的地址就行了,这样就可以对同一块内存区域进行操作,这也就是传指针的原因了。

写了这么多,没这位兄弟写的好写的全,大家还是去看这个链接吧,顺便把水滴石穿C语言系列全看了(强烈推荐),c就到一个高度了:

http://www.yesky.com/174/1864674.shtml

http://blog.csdn.net/canvashat/category/24547.aspx

现在看引用,以下全部摘至林锐的《高质量c++编程》。

引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,n是m的一个引用(reference),m是被引用物(referent)。

int m;

int &n = m;

n相当于m的别名(绰号),对n的任何操作就是对m的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以n既不是m的拷贝,也不是指向m的指针,其实n就是m它自己。

引用的一些规则如下:

(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。

(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

以下示例程序中,k被初始化为i的引用。语句k = j并不能将k修改成为j的引用,只是把k的值改变成为6。由于k是i的引用,所以i的值也变成了6。

int i = 5;

int j = 6;

int &k = i;

k = j; // k和i的值都变成了6;

上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。

以下是“值传递”的示例程序。由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。

void Func1(int x)

{

x = x + 10;

}

int n = 0;

Func1(n);

cout << “n = ” << n << endl; // n = 0

以下是“指针传递”的示例程序。由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。

void Func2(int *x)

{

(* x) = (* x) + 10;

}

int n = 0;

Func2(&n);

cout << “n = ” << n << endl; // n = 10

以下是“引用传递”的示例程序。由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。

void Func3(int &x)

{

x = x + 10;

}

int n = 0;

Func3(n);

cout << “n = ” << n << endl; // n = 10

对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?

答案是“用适当的工具做恰如其分的工作”。

指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?

如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。

二 堆、栈、内存分配、释放

先说说实模式和保护模式http://www.china-askpro.com/msg38/qa23.shtml

8086/8088的微机只有一种工作模式(即实模式)只能处理1M以下的地址(16位),这种地址被城为实地址。后来Intel为了突破1M的内存的限制,推出了386等芯片,增加了保护模式,在32位保护模式下,程序可以访问4G内存空间。但同时为了同以前的程序保持兼容,所以旧程序在实模式下运行,而32位程序可以运行在保护模式下,从而最大地发挥服务器的能力。DOS是运行在实模式的,而Windows 9x/NT都是运行在保护模式的。CPU有专门的保护模式指令。

上面说了,386以后的cpu可以寻址4G的内存空间,虽然我们的微机现在还没配置这么大的内存,为了编程的方便,在操作系统这级引入了虚拟存储机制,我们编写的程序在编译时使用的是虚拟的4G地址空间(windows下实际是2G,还有2G由操作系统管理使用),程序的首地址是0。程序在执行时操作系统会实现虚拟地址到物理地址的映射,即地址重定位,现在的操作系统多采用段页式存储管理,以方便的进行存储保护,存储共享等,更多信息可以参考《操作系统概念 第六版》。

下面对堆和栈的讲解也不知道是那位兄弟写的了。

五大内存分区

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。

堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)

明确区分堆与栈

在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。

首先,我们举一个例子:

void f() { int* p=new int[5]; }

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax

这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。

好了,我们回到我们的主题:堆和栈究竟有什么区别?

主要的区别由以下几点:

1、管理方式不同;

2、空间大小不同;

3、能否产生碎片不同;

4、生长方向不同;

5、分配方式不同;

6、分配效率不同;

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:

打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。

注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)

对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?

动态分配由alloca函数进行分配,calloc分配的是堆内存。

内存管理还是看林锐的《高质量c++编程》吧。

三 结构、类

c的结构中不能有方法,c++的结构和类功能上一样(实现封装,可以继承),只是结构中默认的成员变量或方法是公有的,而类默认的是私有的。很多c的结构体通过宏和函数指针去模拟c++的结构从而实现面对对象,

brew就是这么做的。

四 虚函数、多态

虚函数的主要作用是为了实现多态,多态的意思是当基类的指针指向派生来时,使用这个指针调用的成员函数是派生类的成员函数。

一个类打算做基类的话,它的析构函数要的虚拟的,这样delete基类指针时才会调用派生类的析构函数。

其他还是看附带的文档《c++中的虚函数》。

五 C与C++程序的链接

本节内容完全取至小挺之家blog,他说的很清楚了。

http://blog.codelphi.com/tingxx/archive/2004/10/13/25294.aspx

它们之间的连接问题主要是因为c c++编绎器对函数名译码的方式不同所引起的,考虑下面两个函数:

/* c*/

int strlen(char* string)

{

...

}

//c++

int strlen(char* string)

{

...

}

两个函数完全一样。在c在函数是通过函数名来识别的,而在C++中,由于存在函数的重载问题,函数的识别方式通函数名,函数的返回类型,函数参数列表三者组合来完成的。因此上面两个相同的函数,经过C,C++编绎后会产生完全不同的名字。所以,如果把一个用c编绎器编绎的目标代码和一个用C++编绎器编绎的目标代码进行连接,就会出现连接失败的错误。

解决的方法是使用extern C,避免C++编绎器按照C++的方式去编绎C函数,在头文件中定义:

extern "C" int strlen(char* string);

extern "C"

{

int strlen(char* string);

}

当C编绎器遇到extern "C"的时候就用传统的C函数编译方法对该函数进行编译。由于C编绎器不认识extern "C"这个编绎指令,而程序员又希望C,C++程序能共用这个头文件,因此通常在头文件中使用_cplusplus宏进行区分:

#if define _cplusplus

extern "C"{

#endif

int strlen(char* string)

#ifdefine _cplusplus

}

#endif

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有