宝典第二部分 C/C++程序设计
1.C/C++与Java比较
C/C++更灵活,同一个问题能有多多种答案,Java更注重面向对象的思想(所以牺牲了一部分效率)。C/C++更注重效率,所以更复杂一些。Java将内存管理简单化,c通过malloc和free(),C++通过new和delete来管理内存,一不小心容易出错。
注解:
内存分配方式有三种:
- 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
- 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。只有在堆上分配的内存才需要(也必须)我们进行释放,否则就会造成内存泄漏(栈和堆后面在本文后面)。
例如:此处,a为全局变量,从静态存储区域分配1字节给它;b、c、d为局部变量;b、c的内存在栈上分配;d的内存在堆上分配。d的内存需要在程序退出之前释放掉。char a = 3; char func(char b) { char c = 5; char *d = new char[1]; delete [] d; d = 0; return c; }
2.常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:- 内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行。
检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
- 内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
- 内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
- 忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
- 释放了内存却继续使用它。
有三种情况:
- 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
- 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
- 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
3、指针与数组的对比
C ++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
下面以字符串为例比较指针与数组的特性。
- 修改内容
示例3-1中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 编译器不能发现该错误
cout << p << endl;
示例3.1 修改数组和指针的内容
可以如下操作:
char a[] = "hello";
a[0] = 'x';
std::cout << a << ";大小"<<sizeof(a)<<std::endl;
char *p = "world";
//p[0] = 'x';//内存出错,但是编译器未能发现这个错误;
p = "kliuy";
if (p!=NULL)
{
std::cout << "指针内容"<<p<<";解指针"<<*(p+2) << ";大小" << sizeof(p) <<";指针大小"<<sizeof(char*)<< std::endl;
}
-
内容复制与比较
不能对数组名进行直接复制与比较。示例3-2中,若想把数组a的内容复制给数组b,不能用语句b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。
<span style="font-family: Arial;"></span><pre name="code" class="html">// 数组…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
…
// 指针…
int len = <span style="color:#ff0000;">strlen</span>(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
if(NULL != p)//判断是否分配成功;
{
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
}
free(p);//释放内存,一定要在不用该指针后才释放;
p = NULL;
示例3.2 数组和指针的内容复制与比较
- 计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。示例3-3(a)中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。 注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。示例3-3(b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节
示例3.3(a) 计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字节而不是100字节
}
示例3.3(b) 数组退化为指针
- 指针大小
std::cout << sizeof(char*) << std::endl;//结果=4;
std::cout << sizeof(char) << std::endl;//1
std::cout << sizeof(int*) << std::endl;//4
std::cout << sizeof(int) << std::endl;//4
std::cout << sizeof(double*) << std::endl;//4
std::cout << sizeof(double) << std::endl;//8
指针为什么都是4个字节?因为电脑是32位的地址总线,而指针是一个地址,每一个字节为8位,所以指针的大小是4个字节。
- 修改指针(修改地址)
先看一个错误的例子
void change_ptr(void *ptr, void *dest)
{
ptr = dest;
}
如果是这样,能达到改变指针本身的目的吗?
如果你有迷惑,可以这样看,把类型去掉,把参数ptr传递进去,能改变ptr吗?
显然不能!
那该怎么做才能改变ptr呢?
既然ptr也是一个变量,我要在函数内部改变它,那么就传递一个 指向ptr的指针 !也就是二级指针!
正确的函数形式如下
void change_ptr(void **ptr, void *dest)
{
*ptr = dest;
}
其实指针也是一个变量,我们如果要改变它,必须找到它在内存中的地址,也就是指针的地址。
这里申明一点,想去改变一个变量本身的地址是不可能的,无论你怎么做,在声明(定义)变量的时候,它的地址就由编译器决定好了。
比如这里,你能修改val本身的地址吗?这就如同你能修改常量吗?
4.栈和堆
malloc()到底从哪里得到了内存空间?答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。就是这样! 什么是堆?说到堆,又忍不住说到了栈!什么是栈?下面就另外开个小部分专门而又简单地说一下这个题外话: 什么是堆:堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。 什么是栈:栈是线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。 通过上面对概念的描述,可以知道: 栈是由编译器自动分配释放,存放函数的参数值、局部变量的值等。操作方式类似于数据结构中的栈。 堆一般由程序员分配释放,若不释放,程序结束时可能由OS回收。注意这里说是可能,并非一定。所以我想再强调一次,记得要释放!
4.1数据结构中的栈和堆
栈是在进行操作时遵循后进先出规则的线性结构,而堆是一种特殊的树形数据结构,每个结点都有一个值。
在数据结构中,栈是一种线性表,而且是只可在表的一端进行插入和删除运算的线性表;而堆是一种树形结构,其满中树中任一非叶结点的关键字均不大于或不小于其左右子树的结点的关键字。延伸一点,不同的编程语言在内存分配中就存在堆,栈之分 如:Java中对象创建方式 堆中创建 而C++在堆中或栈中均可创建
5.new和delete(malloc和free上面已经有例子了)
例子很简单,一看就懂(注意malloc才能用free,new的指针才能delete):
int i;
int *p0=&i;
int * p1=new int;
int *p2=new int(2); //*p2初始化值是2
int *p3=new int[1000] //申请1000个单位内存空间
delete p0; //错误的,p0指针不是用new动态申请的
//下面三个是正确的写法
delete p1;
delete p2;
delete[] p3; //注意此处不能用delete p3,因为在申请用了[],则在释放时要用delete[]
<strong>5.1 new和二维数组</strong>
<pre name="code" class="html">定义二维数组char array[x][y];
1.只定义个一维的就可以了
char *array;
array = new char[x*y];
访问的时候*(array+i*y+j)表示array[i][j]
2.定义一个二维数组
char **array1
array1 = new char *[x];
for(i=0;i<x;++i)
<span style="white-space:pre"> </span>array1[i] = new char[y];
...用的时候可以直接array1[i][j]
注意delete
for(i=0;i<x;++i)
<span style="white-space:pre"> </span>delete[] array1[i]; //注意释放顺序;
delete[] array1;
3.要用的方便,可以在array上加定义一个指针变量。
char *array = new char[x*y];
char **array2;
array2 = new char *[x];
for(int i=0;i<x;++i)
array2[i] = array + i*y;
...用起来还是array2[i][j],但二维数组已经是一块连续内存,这是和第二种方法区别的地方,感觉这是比较适合用二维数组的习惯。
delete[] array2;
5.2 new与malloc的区别
- new 是c++中的操作符,malloc是c 中的一个库函数2、new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数
- 内存泄漏对于malloc或者new都可以检查出来的,区别在于new可以指明是那个文件的那一行,而malloc没有这些信息。
- new 和 malloc效率比较
- new可以认为是malloc加构造函数的执行,delete则是free+该类型的析构函数。
- new出来的指针是直接带类型信息的。
- 而malloc返回的都是void指针。
内存泄露的关键就是记录分配的内存和释放内存的操作,看看能不能匹配。跟踪每一块内存的声明周期,例如:每当申请一块内存后,把指向它的指针加入到List中,当释放时,再把对应的指针从List中删除,到程序最后检查List就可以知道有没有内存泄露了。Window平台下的Visual Studio调试器和C运行时(CRT)就是用这个原理来检测内存泄露。
在VS中使用时,需加上
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
crtdbg.h的作用是将malloc和free函数映射到它们的调试版本_malloc_dbg和_free_dbg,这两个函数将跟踪内存分配和释放(在Debug版本中有效)
_CrtDumpMemoryLeaks();
函数将显示当前内存泄露,也就是说程序运行到此行代码时的内存泄露,所有未销毁的对象都会报出内存泄露,因此要让这个函数尽量放到最后。 例子:
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
char *str1 = NULL;
char *str2 = NULL;
str1 = new char[100];
str2 = new char[50];
delete str1;
//_CrtDumpMemoryLeaks();
return 0;
}
可以看到会检测到内存泄露。 但是并没有检测到泄露内存申请的位置,已经加了宏定义#define _CRTDBG_MAP_ALLOC。原因是申请内存用的是new,而刚刚包含头文件和加宏定义是重载了malloc函数,并没有重载new操作符,所以要自己定义重载new操作符才能检测到泄露内存的申请位置。在上面程序中,调用_CrtDumpMemoryLeaks()来检测内存泄露,如果程序可能在多个地方终止,必须在多个地方调用这个函数,这样比较麻烦,可以在程序起始位置调用_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ),这样无论程序何时终止,都会在终止前调用_CrtDumpMemoryLeaks()修改如下:
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#ifdef _DEBUG //重载new
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
char *str1 = NULL;
char *str2 = NULL;
str1 = new char[100];
str2 = new char[50];
delete str1;
//_CrtDumpMemoryLeaks();
return 0;
}