1. 一个由c/C++编译的程序占用的内存分几个部分
堆(heap)和栈(stack)是C/C++编程不可避免会碰到的两个基本概念。首先,这两个概念都可以在讲数据结构的书中找到,他们都是基本的数据结构,虽然栈更为简单一些。
空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
一个由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语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4 申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存(参见《win32的内存分配方式和调试机制 》),他不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数, 在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈(后进先出),然后是参数(先进后出),最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
char *s2 = "bbbbbbbbbbbbbbbbb"; 栈 bbbbbbbbbbbbbbbbbbb 在 常量存储区
aaaaaaaaaaa 是在运行时刻赋值的;
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 读取字符,显然慢了
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
操作系统方面的堆和栈,如上面说的那些,不多说了。
还有就是数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。
虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
2) when function ends, system will automatically free the
这个函数用两个参数、三个参数都可以,只要可以保证缓冲区大小。
三个参数时:
errno_t strcpy_s(
char *strDestination,
size_t numberOfElements,
const char *strSource
);
两个参数时:
errno_t strcpy_s(
char (&strDestination)[size],
const char *strSource
); // C++ only
例子:
输出为:
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。
C++中const关键字允许你指定一个语义约束即指定这是一个“不能被改动”的对象,而编译器会强制实施这项约束。当你确实希望某值保持不变时,你就该确实的说出来,这样编译器就可以帮助你确保这个值不被改变。
STL跌代器iterator 是普通指针,如果在其前面加上const修饰时,则该指针不能被改变,即迭代器不能改变,而const_iterator是一个指向常量的指针。例如:
此处则不希望改变operator *的返回值,假如一个混混沌沌的程序员写出if(a*b = c), 编译器给他错误提示了;如果没有加上const,或许他会抓狂的。
需要提示的是:两个成员函数如果只有常量性不同即一个有const修饰,另一个没有,可以被重载。
看以下class,用来表现一大块文字:
class TextBlock
{
public:
private:
};
TextBlock的operator[]可以被这么使用:
TextBlock tb("hello");
cout<<tb[0];
const TextBlock ctb("hello");
cout<<ctb[0];
注意:(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。
class CTextBlock
{
public:
private:
};
我们可以这么做:
const CTextBlock cctb("hello");
char* pc = &cctb[0];
*pc = 'J'
这导致了所谓的logical constness。这一派认为,一个const成员函数可以修改它所处理的对象内的bit,但只在客户端侦测不出的情况下才得如此。(这种情况更常见,因为我们写程序时使用的是“概念上的常量性”。如:文本内容不能改,但是文本的缓冲长度可以改(假设都是类的成员变量))下面的例子说明了这一用法:
众所周知,我们在const成员函数中不能修改成员变量,但当需要修改时就需要另外一个关键字---mutable.
例如
class MutableTest
{
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:
private:
};
从上面我们看到两个成员函数重复代码太多,当然可以将它们公共代码写成一个函数,再两个都调用那个函数。但是还是重复了一些代码,如:函数调用、两次return语句等。最好的办法是利用一个调用另一个,这促使我们将常量性移除。明显的只能由non-const调用const,如果相反的话,在const函数中本来要保证成员变量不被修改,但是调用non-const那么,上面的保证不能被保证了。所以我们应该这样做:
class TextBlock
{
public:
private:
};
此处有两个转型动作,我们打算让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()
{
}
问题的关键是非const引用形参只能与完全同类型的非const对象关联。
总结:应该将不修改相应实参的形参定义为const引用,如果将这样的形参定义为非const引用,则毫不必要的限制了该函数的使用,普通的非const引用形参在使用时不太灵活。这样的形参不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。