您的位置:首页 > 其它

2016/1/16学习笔记

2016-01-16 22:42 197 查看
1. 一个由c/C++编译的程序占用的内存分几个部分

一、预备知识—程序的内存分配

堆(heap)和栈(stack)是C/C++编程不可避免会碰到的两个基本概念。首先,这两个概念都可以在讲数据结构的书中找到,他们都是基本的数据结构,虽然栈更为简单一些。

在具体的C/C++编程框架中,这两个概念并不是并行的。对底层机器代码的研究可以揭示,栈是机器系统提供的数据结构,而堆则是C/C++函数库提供的。

具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。

机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的 ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因.

和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存

空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:

1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。

2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。

3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

一个由c/C++编译的程序占用的内存分为以下几个部分

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放

4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

二、例子程序

这是一个前辈写的,非常详细

//main.cpp

int a = 0; 全局初始化区

char *p1; 全局未初始化区

main()

{

int b; 栈

char s[] = "abc"; 栈

char *p2; 栈

char *p3 = "123456"; 123456/0在常量区,p3在栈上,可否看成是一种映射。

static int c =0; 全局(静态)初始化区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

分配得来得10和20字节的区域就在堆区。

strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方,一个常量区块可能对应(映射)多个变量区,优化内存。

}

二、堆和栈的理论知识

2.1申请方式

stack:

由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

heap:

需要程序员自己申请,并指明大小,在c中malloc函数

如p1 = (char *)malloc(10);

在C++中用new运算符

如p2 = (char *)malloc(10);

但是注意p1、p2本身是在栈中的,但是分别为其的内存区间在堆(heap)区。

2.2

申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4申请效率的比较:

栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存(参见《win32的内存分配方式和调试机制 》),他不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5堆和栈中的存储内容

栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈(后进先出),然后是参数(先进后出),最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6存取效率的比较
char s1[] = "aaaaaaaaaaaaaaa";栈

char *s2 = "bbbbbbbbbbbbbbbbb";栈 bbbbbbbbbbbbbbbbbbb在常量存储区

aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快
对应的汇编代码

10: a = c[1];

00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]

0040106A 88 4D FC mov byte ptr [ebp-4],cl

11: a = p[1];

0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]

00401070 8A 42 01 mov al,byte ptr [edx+1]

00401073 88 45 FC mov byte ptr [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了
2.7小结:

堆和栈的区别可以用如下的比喻来看出:

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
堆和栈的区别主要分:

操作系统方面的堆和栈,如上面说的那些,不多说了。

还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。

虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
另有说法:(仅供参考)
1) about stack, system will allocate memory to the instance
of object automatically, and to the heap, you must allocate
memory to the instance of object with new or malloc manually.

2) when function ends, system will automatically free the
memory area of stack, but to the heap, you must free the
memory area manually with free or delete, else it will result
in memory leak.
大致意思如下:
stack上分配的内存系统自动释放,heap上分配的内存,系统不释放,哪怕程序退出,那一块内存还是在那里。stack一般是静态分配内存,heap上一般是动态分配内存

2. strcpy_s与strcpy的比较

strcpy_s和strcpy()函数的功能几乎是一样的。strcpy函数,就象gets函数一样,它没有方法来保证有效的缓冲区尺寸,所以它只能假定缓冲足够大来容纳要拷贝的字符串。在程序运行时,这将导致不可预料的行为。用strcpy_s就可以避免这些不可预料的行为。

这个函数用两个参数、三个参数都可以,只要可以保证缓冲区大小。

三个参数时:

errno_t strcpy_s(

char *strDestination,

size_t numberOfElements,

const char *strSource

);

两个参数时:

errno_t strcpy_s(

char (&strDestination)[size],

const char *strSource

); // C++ only

例子:

#include<iostream>
#include<cstring>
using namespace std;

void Test(void)
{
char *str1=NULL;
str1=new char[20];
char str[7];
strcpy_s(str1,20,"hello world");//三个参数
strcpy_s(str,"hello");//两个参数但如果:char *str=new char[7];会出错:提示不支持两个参数
cout<<"strlen(str1):"<<strlen(str1)<<endl<<"strlen(str):"<<strlen(str)<<endl;
printf(str1);
printf("\n");
cout<<str<<endl;
}

int main()
{
Test();
return 0;
}

#include<iostream>

#include<string.h>

using namespace std;

void Test(void)

{

char *str1=NULL;

str1=new char[20];

char str[7];

strcpy_s(str1,20,"hello world");//三个参数

strcpy_s(str,"hello");//两个参数但如果:char *str=new char[7];会出错:提示不支持两个参数

cout<<"strlen(str1):"<<strlen(str1)<<endl<<"strlen(str):"<<strlen(str)<<endl;

printf(str1);

printf("\n");

cout<<str<<endl;

}

int main()

{

Test();

return 0;

}

输出为:

strlen(str1): 11 //另外要注意:strlen(str1)是计算字符串的长度,不包括字符串末尾的“\0”!!!

strlen(str): 5

hello world

hello

3.


const char*, char const*, char*const的区别

 const char*, char const*, char*const的区别问题几乎是C++面试中每次都会有的题目。 这个知识易混点之前是看过了,今天做Linux上写GTK程序时又出现个Warning,发散一下又想到这个问题,于是翻起来重嚼一下。

事实上这个概念谁都有只是三种声明方式非常相似:

Bjarne在他的The C++ Programming Language里面给出过一个助记的方法:

把一个声明从右向左读。

char * const cp; ( * 读成 pointer to ) cp is a const pointer to char

const char * p; p is a pointer to const char;

char const * p; 同上因为C++里面没有const*的运算符,所以const只能属于前面的类型。

C++标准规定,const关键字放在类型或变量名之前等价的。

const int n=5; //same as below

int const m=10

结论:

char * const cp : 定义一个指向字符的指针常数,即const指针

const char* p : 定义一个指向字符常数的指针

char const* p : 等同于const char* p

const char **是一个指向指针的指针,那个指针又指向一个字符串常量。

char **也是一个指向指针的指针,那个指针又指向一个字符串变量。
Const用法总结

第一:尽可能使用const。

C++中const关键字允许你指定一个语义约束即指定这是一个“不能被改动”的对象,而编译器会强制实施这项约束。当你确实希望某值保持不变时,你就该确实的说出来,这样编译器就可以帮助你确保这个值不被改变。

const可以在class外部修饰global或namespace作用域中的常量,可以修饰文件、函数、或区块作用域中被static声明的对象,可以修饰classes内部的static或non-static成员变量,也可以修饰常量指针、指针常量、指向常量的指针常量;const最有威力的用法还是在函数声明时,可以和函数的返回值,各个参数,函数自身产生关联。

const修饰变量时,如果const关键字出现在星号的左边,表示被指物是常量(没有星号时(即非指针变量)也适用),如果出现在星号的右边,则表示指针自身是常量,如果在星号两边都出现,则被指物和指针两者都是常量。

const int i = 10;

char greeting [] = "hello world";

char greeting2[] = "qu world";

const char *p = greeting;

char const *q = greeting; // p和q都是常量指针

char* const cq = greeeting; // cq为指针常量,不能改变它的指向

*p = "aaaa";// error

P = greeting2; //ok

cq = greeting2; // error

*cq ='a'; //ok

const char* const pq = greeting; //pq是指向常量的指针常量

STL跌代器iterator 是普通指针,如果在其前面加上const修饰时,则该指针不能被改变,即迭代器不能改变,而const_iterator是一个指向常量的指针。例如:

std::vector<int > vec;

const std::vector<int>::iterator iter = vec.begin();

*vec = 10 ;//ok

++vec; // error

std::vector<int>::const_iterator cIter = vec.begin();

*vec = 10;//error

++vec; //ok

const修饰函数时,可以给参数加上const不希望修改参数的值,也可以给返回值加上const不希望无意中改变返回值。例如:

class aa{.....};

const aa operator*(const aa& a1,const aa& a2);

此处则不希望改变operator *的返回值,假如一个混混沌沌的程序员写出if(a*b = c), 编译器给他错误提示了;如果没有加上const,或许他会抓狂的。

在c++面向对象编程中,常用const来修饰类的成员函数,这样做是为了确认该成员函数可以作用于const对象身上。这样做有两个重要理由:第一、它们使class接口比较容易理解,得知哪个函数可以改动对象内容而哪个函数不可以,例如常见的get/set函数;第二、它们使“操作const对象”成为可能,这对编写高效代码是个关键。

需要提示的是:两个成员函数如果只有常量性不同即一个有const修饰,另一个没有,可以被重载。

看以下class,用来表现一大块文字:

class TextBlock

{

public:

const char& operator[](std::size_t position)const //operator [] for const对象

{return text[position];}

Char& operator[](std::size_tposition) //operator[] for non-const 对象

{return text[position];}

private:

string text;

};

TextBlock的operator[]可以被这么使用:

TextBlock tb("hello");

cout<<tb[0]; //调用non-const TextBlock::operator[]

const TextBlock ctb("hello");

cout<<ctb[0]; //调用const TextBlock::operator[]

注意:(1)真实程序中const对象大多用于passed by pointer-to-const或passed by reference-to-const的传递结果.(2)上面的const版本返回的是const引用,这使得返回的结果不能作为左值,或者即使作为左值也不能改变返回的内容。

如:cout<<ctb[0];//正确

ctb[0]='x'; //错误

(3)如果函数的返回类型是个内置类型,那么改动函数返回值也是不合法的。

第二:bitwise constness 和 logical constness。

bitwise constness阵营的人认为,成员函数只有在不更改对象之任何成员(static除外)时才可以说是const。也就是说它不得更改对象内的任何一个bit。这种论点这是C++对常量的定义,因为它只需寻找成员变量的赋值动作即可。因此const成员函数不可以更改对象内的任何non-static成员变量。

不幸的许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体的说,一个更改了“指针所指物”的成员函数虽然不算是const,但如果只有指针(而非所指物)录属与对象,那么称此函数为bitness const不会引发编译器的异议。这导致反直观结果。如:

class CTextBlock

{

public:

const char& operator[](std::size_t position)const //operator [] for const对象

{return pText[position];}

private:

char* pText;

};

我们可以这么做:

const CTextBlock cctb("hello");

char* pc = &cctb[0]; //注意: pc不是const指针也不会出错。

*pc = 'J' //现在cctb为"Jello"

这导致了所谓的logical constness。这一派认为,一个const成员函数可以修改它所处理的对象内的bit,但只在客户端侦测不出的情况下才得如此。(这种情况更常见,因为我们写程序时使用的是“概念上的常量性”。如:文本内容不能改,但是文本的缓冲长度可以改(假设都是类的成员变量))下面的例子说明了这一用法:

众所周知,我们在const成员函数中不能修改成员变量,但当需要修改时就需要另外一个关键字---mutable. mutable释放掉non-static成员变量的logical constness约束。

例如

class MutableTest

{

public:

   MutableTest();

~MutableTest();

  void Output() const;

   int GetOutputTimes() const;

private:

   mutable int m_iTimes;

};

MutableTest::MutableTest()

{

 m_iTimes = 0;

}

MutableTest::~MutableTest()

{}

void MutableTest::Output() const

{

 cout << "Output for test!" << endl;

 m_iTimes++;



int MutableTest::GetOutputTimes() const

{

 return m_iTimes;

}

在这个例子中,将Output函数修饰为const,却希望记录输出的次数,这时就需要改变m_iTimes的值,这个时候就需要mutable关键字来突破const关键字的限制了:m_iTimes在常成员函数中也可以被改变。

第三:在const和non-const成员函数中避免重复。

看下面的例子:

class TextBlock

{

public:

const char& operator[](std::size_t position)const //operator [] for const对象

{

很多处理

return text[position];

}

Char& operator[](std::size_tposition) //operator[] for non-const 对象

{

很多处理

return text[position];

}

private:

string text;

};

从上面我们看到两个成员函数重复代码太多,当然可以将它们公共代码写成一个函数,再两个都调用那个函数。但是还是重复了一些代码,如:函数调用、两次return语句等。最好的办法是利用一个调用另一个,这促使我们将常量性移除。明显的只能由non-const调用const,如果相反的话,在const函数中本来要保证成员变量不被修改,但是调用non-const那么,上面的保证不能被保证了。所以我们应该这样做:

class TextBlock

{

public:

const char& operator[](std::size_t position)const //operator [] for const对象

{

很多处理

return text[position];

}

Char& operator[](std::size_t position) //operator[] for non-const 对象

{

return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);

}

private:

string text;

};

此处有两个转型动作,我们打算让non-const调用const,但non-const内部若是单纯的调用operator[],会递归调用自己。为了避免无穷递归,我们必须明确指出调用的是const,但是C++缺乏直接的支持,因此我们将*this从原始类型TextBlock& 转型为const TextBlock&。我们使用转型操作为它加上const!两次转型操作为:第一次用来为*this添加上const(这使接下来调用operator[]时得以调用const版本。这次是安全转型,所以我们使用static_cast),第二次是从const
operator[]的返回值中移除const(使用const_cast完成,没有其他的选择,就技术而言是有的,一个C-style转型也可以,但那种转型很少是正确的抉择)。

总结:const可以被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体,将某些东西声明为const可以帮助编译器侦测错误用法。当需要在const的成员函数内部修改成员变量时,可以求助于mutable关键字。

第四:更灵活的指向const的引用。

如果函数具有普通的非const引用形参,则显然不能通过const对象进行调用。毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的const特性。但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的,如:

int incr(int& val)

{return ++val;}

int main()

{

short v1 = 0;

const int v2 = 42;

int v3 = incr(v1); //错误:v1不是int,需要转换;

v3 = incr(v2); //错误:v2是const

v3 = incr(0); //错误: 字面值不是左值(是右值)

v3 = incr(v1 + v2); //错误:加法不会产生一个左值(是右值)

int v4 = incr(v3); //OK, v3是一个non-const 的int

}

问题的关键是非const引用形参只能与完全同类型的非const对象关联。

总结:应该将不修改相应实参的形参定义为const引用,如果将这样的形参定义为非const引用,则毫不必要的限制了该函数的使用,普通的非const引用形参在使用时不太灵活。这样的形参不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: