面试知识点总结之c++基础(一)

1、#include<>和#include""的区别

标准库用<>,其余用""。

注:

用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。

用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。

2、C++中的static

C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。
一、面向过程设计中的static
1、静态全局变量
在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量。我们先举一个静态全局变量的例子,如下:
  

#include<iostream>
using namespace std;

static int n;  //定义静态全局变量

void fn()
{
	n++;
	cout<<n<<endl;
}

int main(void)
{
	n = 20;
	cout<<n<<endl;
	fn();
	return 0;
}


静态全局变量有以下特点:

  • 该变量在全局数据区分配内存;
  • 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
  • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的; 
静态变量都在全局数据区分配内存,包括后面将要提到的静态局部变量。对于一个完整的程序,在内存中的分布情况如下图:
代码区
全局数据区
堆区
栈区

  一般程序的由new产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。细心的读者可能会发现,Example 1中的代码中将

static int n;  //定义静态全局变量

改为

int n;  //定义全局变量
程序照样正常运行。
的确,定义全局变量就可以实现变量在文件中的共享,但定义静态全局变量还有以下好处:
       静态全局变量不能被其它文件所用;
       其它文件中可以定义相同名字的变量,不会发生冲突;
您可以将上述示例代码改为如下:

//File1
#include<iostream>
using namespace std;

void fn();
static int n;  //定义静态全局变量

int main(void)
{
	n = 20;
	cout<<n<<endl;
	fn();
	return 0;
}

//File2
#include<iostream>
using namespace std;

extern int n;

void fn()
{
	n++;
	cout<<n<<endl;
}


编译并运行这个程序,您就会发现上述代码可以分别通过编译,但运行时出现错误。试着将

static int n;  //定义静态全局变量

改为

int n;  //定义全局变量
再次编译运行程序,细心体会全局变量和静态全局变量的区别。
2、静态局部变量
在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量。
我们先举一个静态局部变量的例子,如下:

#include<iostream>
using namespace std;

void fn();

int main(void)
{
	fn();
	fn();
	fn();
	return 0;
}

void fn()
{
	static int n = 10;
	cout<<n<<endl;
	n++;
}


通常,在函数体内定义了一个变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。
  但有时候我们需要在两次调用之间对变量的值进行保存。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。
  静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。
静态局部变量有以下特点:
    (1)该变量在全局数据区分配内存;
    (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
    (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
    (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
3、静态函数

  在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
静态函数的例子:

#include<iostream>
using namespace std;

static void fn();   //声明静态函数

int main(void)
{
	fn();
	return 0;
}

void fn()     //定义静态函数
{
	int n = 10;
	cout<<n<<endl;
}


定义静态函数的好处:
       静态函数不能被其它文件所用;
       其它文件中可以定义相同名字的函数,不会发生冲突;
二、面向对象的static关键字(类中的static关键字)
1、静态数据成员
在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。先举一个静态数据成员的例子。

#include<iostream>
using namespace std;

class Myclass
{
private:
	int a , b , c;
	static int sum;  //声明静态数据成员
public:
	Myclass(int a , int b , int c);
	void GetSum();
};

int Myclass::sum = 0;   //定义并初始化静态数据成员

Myclass::Myclass(int a , int b , int c)
{
	this->a = a;
	this->b = b;
	this->c = c;
	sum += a+b+c;
}
void Myclass::GetSum()
{
	cout<<"sum="<<sum<<endl;
}

int main(void)
{
	Myclass M(1 , 2 , 3);
	M.GetSum();
	Myclass N(4 , 5 , 6);
	N.GetSum();
	M.GetSum();
	return 0;
}


可以看出,静态数据成员有以下特点:

  • 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;
  • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。在Example 5中,语句int Myclass::Sum=0;是定义静态数据成员;
  • 静态数据成员和普通数据成员一样遵从public,protected,private访问规则;
  • 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它;
  • 静态数据成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式为:
    <数据类型><类名>::<静态数据成员名>=<值>
  • 类的静态数据成员有两种访问形式:
    <类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
    如果静态数据成员的访问权限允许的话(即public的成员),可在程序中,按上述格式来引用静态数据成员 ;
  • 静态数据成员主要用在各个对象都有相同的某项属性的时候。比如对于一个存款类,每个实例的利息都是相同的。所以,应该把利息设为存款类的静态数据成员。这有两个好处,第一,不管定义多少个存款类对象,利息数据成员都共享分配在全局数据区的内存,所以节省存储空间。第二,一旦利息需要改变时,只要改变一次,则所有存款类对象的利息全改变过来了;
  • 同全局变量相比,使用静态数据成员有两个优势:
  1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
2、静态成员函数

  与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。下面举个静态成员函数的例子。

#include<iostream>
using namespace std;

class Myclass
{
private:
	int a , b , c;
	static int sum;  //声明静态数据成员
public:
	Myclass(int a , int b , int c);
	static void GetSum();  //声明静态成员函数
};

int Myclass::sum = 0;   //定义并初始化静态数据成员

Myclass::Myclass(int a , int b , int c)
{
	this->a = a;
	this->b = b;
	this->c = c;
	sum += a+b+c;    //非静态成员函数可以访问静态数据成员
}
void Myclass::GetSum()    //静态成员函数的实现
{
	//cout<<a<<endl;    //错误代码,a是非静态数据成员
	cout<<"sum="<<sum<<endl;
}

int main(void)
{
	Myclass M(1 , 2 , 3);
	M.GetSum();
	Myclass N(4 , 5 , 6);
	N.GetSum();
	Myclass::GetSum();
	return 0;
}

关于静态成员函数,可以总结为以下几点:

  • 出现在类体外的函数定义不能指定关键字static;
  • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
  • 静态成员函数不能访问非静态成员函数和非静态数据成员;
  • 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;
  • 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以直接使用如下格式:
    <类名>::<静态成员函数名>(<参数表>)
    调用类的静态成员函数。
三、C/C++宏的定义

 宏替换是C/C++系列语言的技术特色,C/C++语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等。

    我们常用的宏替换主要有这么几个类型。
1.宏常量
    在ACM等算法竞赛中,经常会把数组的最大下标通过宏定义的方法给出,以方便调试,例如:
#define MAX 1000

int array[MAX][MAX]
......

for(int i = 0; i < MAX; i++)
......

    将一个数字定义成全局的常量,这个用法在国产垃圾教材上十分常见。但在经典著作《Effective C++》中,这种做法却并不提倡,书中更加推荐以const常量来代替宏常量。因为在进行词法分析时,宏的引用已经被其实际内容替换,因此宏名不会出现在符号表中。所以一旦出错,看到的将是一个无意义的数字,比如上文中的1000,而不是一个有意义的名称,如上文中的MAX。而const在符号表中会有自己的位置,因此出错时可以看到更加有意义的错误提示。

2.用于条件编译标识的宏
#define常与#ifdef/#ifndef/defined指令配合使用,用于条件编译。
#ifndef _HEADER_INC_
#define _HEADER_INC_
……
……
#endif
    这种宏标记在头文件中十分常见,用于防止头文件被反复包含。应该养成习惯在每个头文件中都添加这种标记。
    还有一种用于条件编译的用法
#ifdef DEBUG
printf("{“}Debug information\n");
#endif
    通过DEBUG宏,我们可以在代码调试的过程中输出辅助调试的信息。当DEBUG宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果。
 
在条件编译时建议使用#if defined和#if !defined来代替使用#ifdef/#ifndef,因为前者更方便处理多分支的情况与较复杂条件表达式的情况。#ifdef/#ifndef只能处理两个分支:#ifdef/#ifndef,#else,#endfi;#if defined和#if !defined可以处理多分支的情况:#if defined/#if !defined, #elif defined, #else, #endif。#ifdef只能判断是否定义,但是#if defined可以判断复杂的表达式的值是否为真。
#if defined(OS_HPUX)&&(defined(HPUX_11_11)|| defined(HPUX_11_23) 
// for HP-UX 11.11 and 11.23 
#elif defined(OS_HPUX) && defined(HPUX_11_31 
// for HP-UX 11.31 
#elif defined(OS_AIX) 
// for AIX 
#else 
… 
#endif

条件编译时,如果一个文件中太多条件编译的代码,有些编辑器的智能感知可能都不能很好地解析,还是保持代码越简单越好。对于函数级别的条件编译主要有两种实现方式: 
(1) 同一个函数声明,同一个函数定义,函数体内使用条件编译代码。这种方式有个问题,如果条件编译代码太多,会导致这个函数体很长,不利于阅读与维护;有一个优点是,有利于编辑器的智能感知,因为这样解析函数名比较方便,但随着编辑器功能的完善,这方面的差别就不明显了。
(2) 根据编译条件,将编译条件相同的代码放到单独的文件中,这些文件在顶层文件中使用条件编译指令来引用。这种方式最大的优点就是不同平台的程序由不同的源文件来实现,很便于多人分工合作,对于某一部分代码由一个人实现并测试完成后直接把源文件复制过来就可以了,进行低层次的单元测试非常方便;它的缺点就是增加了目录中的文件数量。

3.宏函数
    宏函数的语法有以下特点:
    (1)、如果需要换行,则行末要加反斜杠“\”表示换行。宏函数体的最后一行不加反斜杠。
    (2)、假设有参数ARGU,值为argu,则所有的ARGU被直接替换为argu,#ARGU被认为是字符串,会被替换成"argu"(带引号)。
    (3)、由于宏函数是直接替换,所有一般情况下对宏函数的调用时行末不需要加分号。
 
宏函数的作用:
1)、避免函数调用,提高程序效率
常用的就是最大值与最小值的判断函数,由于函数内容并不多,如果定义为函数在调用比较频繁的场合会明显降低程序的效率,其实宏是用空间效率换取了时间效率。如取两个值的最大值: 
#define MAX(a,b) ((a)<(b) ? (b) : (a))
定义为函数: 
inline int Max(int a, int b)
{
 return a<b ? b : a;
}
定义为模板: 
template <typename T> 
inline T TMax(T a, T b)
{
 return a < b ? b : a ;
}
使用宏函数的优点有两个:
(1)适用于任何实现了operator<的类型,包括自定义类型;
(2)效率最高。虽然使用inline提示符也将函数或模板定义为内联的,但这只是一种提示而已,到底编译器有没有优化还依赖于编译器的实现,而使用宏函数则是完全由代码本身控制。 
需要注意的是,由于宏的本质是直接的文本替换,所以在宏函数的“函数体”内都要把参数使用括号括起来,防止参数是表达式时造成语法错误或结果错误,如:
#define MIN( a, b) b < a ? b : a 
#define SUM( a, b) a + b 
cout<<MIN(3,5)<<endl; // 语法错误:cout<<b < a ? b : a<<endl; 
int c = SUM(a,b)*2;  // c的期望值:16,实际值:13

2)、引用编译期数据
上述的这些作用虽然使用宏函数可以取得更好的性能,但如果从功能上讲完全可以不使用宏函数,而使用模板函数或普通函数实现,但还有些时候只能通过宏实现。例如,程序中在执行某些操作时可能会失败,此时要打印出失败的代码位置,只能使用宏实现。 
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n' 
if( 0 != rename("oldFileName", "newFileName") )

 cout<<"failed to move file"<<endl; 
 SHOW_CODE_LOCATION(); 
}
虽然宏是简单的替换,所以在调用宏函数SHOW_CODE_LOCATION时,分号可以直接写到定义里,也可以写到调用处,但最好还是写到调用处,看起来更像是调用了函数,否则看着代码不伦不类,如:
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n' 
if( 0 != rename("oldFileName", "newFileName") )

 cout<<"failed to move file"<<endl; 
 SHOW_CODE_LOCATION()
}

3)、do-while的妙用
do-while循环控制语句的特点就是循环体内的语句至少会被执行一次,如果while(…)内的条件始终为0时,循环体内的语句就会被执行且只被执行一次,这样的执行效果与直接使用循环体内的代码相同,但这们会得到更多的益处。 
#define SWAP_INT(a, b) do
{\
 int tmp = a; \
 a = b; \
 b = tmp; \
}while(0)

int main( void ) 

 int x = 3, y = 4;
 if( x > y )
 {
  SWAP_INT(x, y);
 }
 return 0;
}
通过do-while代码块的宏定义我们不仅可以把SWAP_INT像函数一样用,而且还有优点: 
(1)、在宏定义中可以使用局部变量; 
(2)、在宏定义中可以包含多个语句,但可以当作一条语句使用,如代码中的if分支语句,如果没有do-while把多条语句组织成一个代码块,则程序的运行结果就不正确,甚至不能编译。 
其实我们定义的SWAP_INT(a, b)相当于定义了引用参数或指针参数的函数,因为它可以改变实参的值。在C++0X中有了decltype关键词,这种优势就更显示了,因为在宏中使用了局部变量必须确定变量的类型,所以这个宏只能用于交换int型的变量值,如果换作其它类型则还必须定义新的宏,如SWAP_FLOAT、SWAP_CHAR等,而通过decltype,我们就可以定义一个万能的宏。 

#include <iostream> 
using namespace std; 
#define SWAP(a, b) do
{ 
 decltype(a) tmp = a; 
 a = b; 
 b = tmp; 
}while(0)
int main( void ) 
{ 
 int a = 1, b = 2; 
 float f1 = 1.1f, f2 = 2.2f; 
 SWAP(a, b); 
 SWAP(f1,f2); 
 return 0; 
}


通过宏实现的SWAP“函数”要比使用指针参数效率还要高,因为它连指针参数都不用传递而是使用直接代码,对于一些效率要求比较明显的场合,宏还是首选。

4、取消宏定义
#undef指令用于取消前面用#define定义的宏,取消后就可以重新定义宏。该指令用的并不多,因为过多的#undef会使代码维护起来非常困难,一般也只用于配置文件中,用来清除一些#define的开关,保证宏定义的唯一性。
// config.h 
#undef HAS_OPEN_SSL 
#undef HAS_ZLIB 
#if defined(HAS_OPEN_SSL) 
… 
#endif 
#if defined(HAS_ZLIB) 
… 
#endif
将对该头文件的引用放到所有代码文件的第一行,就可以保证HAS_OPEN_SSL没有被定义,即使是在编译选项里定义过一宏,也会被#undef指令取消,这样使得config.h就是唯一一处放置条件编译开关的地方,更有利于维护。

5、注意事项
1)、普通宏定义
(1)宏名一般用大写
(2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。
(3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
(4)宏定义末尾不加分号;
(5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。
(6)可以用#undef命令终止宏定义的作用域
(7)宏定义可以嵌套
(8)字符串""中永远不包含宏
(9)宏定义不分配内存,变量定义分配内存。
2)、带参宏定义
(1)实参如果是表达式容易出问题
(2)宏名和参数的括号间不能有空格
(3)宏替换只作替换,不做计算,不做表达式求解
(4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存
(5)宏的哑实结合不存在类型,也没有类型转换。
(6)函数只有一个返回值,利用宏则可以设法得到多个值
(7)宏展开使源程序变长,函数调用不会
(8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)

6、关于#和##
在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。比如下面代码中的宏:
#define WARN_IF(EXP)    
    do{ if (EXP)    
            fprintf(stderr, "Warning: " #EXP "\n"); }   
    while(0)
那么实际使用中会出现下面所示的替换过程:
WARN_IF (divider == 0);

 被替换为

do {
    if (divider == 0)
  fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);
这样每次divider(除数)为0的时候便会在标准错误流上输出一个提示信息。
而##被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。比如你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:
struct command
{
 char * name;
 void (*function) (void);
};

#define COMMAND(NAME) { NAME, NAME ## _command }

// 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了:

struct command commands[] = {
 COMMAND(quit),
 COMMAND(help),
 ...
}
COMMAND宏在这里充当一个代码生成器的作用,这样可以在一定程度上减少代码密度,间接地也可以减少不留心所造成的错误。我们还可以n个##符号连接 n+1个Token,这个特性也是#符号所不具备的。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d

typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);
// 这里这个语句将展开为:
//  typedef struct _record_type name_company_position_salary;

7、关于...的使用
在C宏中称为Variadic Macro,也就是变参宏。比如:
#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)

 // 或者

#define myprintf(templt,args...) fprintf(stderr,templt,args)
第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要求我们必须写成:
myprintf(templt,);
的形式。这时的替换过程为:
myprintf("Error!\n",);

 替换为:
 
fprintf(stderr,"Error!\n",);
这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:
myprintf(templt);
而它将会被通过替换变成:
fprintf(stderr,"Error!\n",);
很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU CPP都支持下面的宏定义方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
myprintf(templt);

 被转化为:

fprintf(stderr,templt);
这样如果templt合法,将不会产生编译错误。 这里列出了一些宏使用中容易出错的地方,以及合适的使用方式。


四、C++内存分配详解

 ,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

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

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

  全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

  常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

  明确区分堆与栈

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

  首先,我们举一个例子:

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

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

push         14h

  0040102A    call         operator new (00401060)

 0040102F    add          esp,4
   mov          dword ptr [ebp-8],eax
   mov          eax,dword ptr [ebp-8]
   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种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

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

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

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

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

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

  static 用来控制变量的存储方式和可见性

  函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此 函数控制)。需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

  static 的内部机制

  静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的 main()函数前的全局数据声明和定义处。

  静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

  static 被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。

  static 的优势

  可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的 值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。引用静态数据成员时,采用如下格式:

  <类名>::<静态成员名>

  如果静态数据成员的访问权限允许的话(即 public 的成员),可在程序中,按上述格式来引用静态数据成员。

  

Ps

  (1) 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。

  (2) 不能将静态成员函数定义为虚函数。

  (3) 由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊,变量地址是指向其数据类型的指针,函数地址类型是一个“nonmember 函数指针”。

  (4) 由于静态成员函数没有 this 指针,所以就差不多等同于 nonmember 函数,结果就产生了一个意想不到的好处:成为一个 callback 函数,使得我们得以将 c++ 和 c-based x window 系统结合,同时也成功的应用于线程函数身上。

  (5) static 并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。

  (6) 静态数据成员在<定义或说明>时前面加关键字 static。

  (7) 静态数据成员是静态存储的,所以必须对它进行初始化。

  (8) 静态成员初始化与一般数据成员初始化不同:

  初始化在类体外进行,而前面不加 static,以免与一般静态变量或对象相混淆;

  初始化时不加该成员的访问权限控制符 private、public;

  初始化时使用作用域运算符来标明它所属类;

  所以我们得出静态数据成员初始化的格式:

  <数据类型><类名>::<静态数据成员名>=<值>

   (9) 为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值