1、程序的内存空间
1.1、堆和栈
堆
(
h
e
a
p
)
(heap)
(heap)和栈
(
s
t
a
c
k
)
(stack)
(stack)都是一种内存空间。
栈空间的分配是由系统自动完成的,用来存放各种变量和寄存器,无论是全局变量还是局部变量,都存储在其中。同时每个程序线程都有属于自己的栈空间,用以保证之间互不干扰。
而在一个线程当中的每一个函数在被调用的时候,也会被分配一些属于自己的栈空间,用以存放该函数所用到的局部变量和一些必要的寄存器信息。当函数被调用结束,其对应的栈的空间也会被释放。因此,局部变量的生存周期终止于栈空间被释放。
堆空间的分配则需要人为干涉了,它可以用来存放一些临时变量,其空间相来说也大一些。更加不同于栈的一点是,堆是任何函数都可以访问的,不存在局限性。
1.2、类的空间分配
1、类被实例化之前是不占内存的。
2、类的对象所占内存的大小只与成员变量有关,与成员函数无关。
3、类的成员函数和其他代码一样,被放在代码区,所有对象共享一套函数代码,但有自己的栈区存放成员变量。
4、因此类在被实例化之前之所以不占空间,不是真的不占空间,而是因为其成员函数和其他必要代码被放在代码区。所以总是能在实例化之前找到成员函数的地址的。
1.3、类的静态属性和静态方法
1.3.1、静态属性
类的属性大致分为两类,分别是静态属性和实例属性。其中实例属性又分为私有属性、公有属性、内置属性。
其中静态属性归类所有,不需要实例化也可以调用,放在数据段(静态存储区)。而实例属性归对象所有,放在栈区,必须实例化之后才能使用。
举个例子:
#include<iostream>
using namespace std;
class Car
{
public:
Car(void){
cout<<"this is a class of Car"<<endl;
}
~Car(void){
cout<<"Car's class is over"<<endl;
}
static int temp; //这里只是声明而已
};
int Car::temp = 9; //必须得在全局区域给它初始化
int main()
{
cout<<"the number of temp is: "<<Car::temp<<endl;
return 0;
}
这里有几个点需要注意:
1、静态属性和静态方法,只能放在
p
u
b
l
i
c
public
public下。
2、定义一个全局变量和静态变量有两个步骤,第一步是声明变量,声明之后的变量会被放在
B
S
S
(
b
l
o
c
k
s
t
r
a
t
e
d
b
y
s
y
m
b
o
l
)
BSS(block strated by symbol)
BSS(blockstratedbysymbol)区域,等到第二部初始化完毕,才会被放在数据段(
D
a
t
a
Data
Data)中去。
3、初始化静态属性必须得在全局作用域中,而不能在
m
a
i
n
main
main函数里,因为静态属性的作用域就是全局。
1.3.1、静态方法
类的方法也大致可以分为静态方法和实例方法,其中静态方法也是归类所有,不需要实例化便可以调用,而实例方法需要实例化之后才能调用。但与属性不同的是,静态方法和实例方法,都是放在文本段(代码区),用同一个内存空间。
2、指针
指针是C语言学习的时候用到,但这里再次记录一下,以便理解后面的内容。
2.1、指针的本质
指针是一种存放地址的特殊变量,它也有属于自己的地址。因此,指针并不等于地址!!!
我们可以通过以下代码查看指针存放的地址,以及指针本身的地址。
void main()
{
int *point;
printf("point's value is %p\n",point);
printf("point's address is %p\n",&point);
}
再通过以下几个例子可以进一步了解指针本质。
错误示例:
int *p;
*p = 9;
错误原因:这里 p p p指针存放的是 N U L L NULL NULL的地址,所以这句代码相当于让 N U L L NULL NULL地址对应的变量 ( N U L L ) (NULL) (NULL)去存放9这个数。而 N U L L NULL NULL即是空,让空去存放东西,显然是错误的。
正确示例1:
int *p;
int a;
p = &a;
a = 9;
正确示例2:
int *p;
int a;
p = &a;
*p = 9;
正确原因:通过
p
=
p=
p=&
a
a
a;这句代码让p指针存放a变量的地址,那么无论是直接修改a,还是通过地址索引a再修改之,结果都是一样的。
此外,由于在32为计算机,地址则占4个字节,在64位计算机中,地址占6个字节或8个字节的缘故,指针的大小也是其对应地址所占的空间。
2.2、指针的意义
指针可以说是C语言的灵魂,即便在C++当中也发挥着巨大的作用。以下对于指针的意义仅是我个人学习过程中的所感所获。
意义一:在大部分程序语言中,函数的返回值一般分为两种情况,一种是返回一个值,另一种则是返回一个地址。我们称之为传值和传址。而传址的目的,还是为了获得目标值,所以此时用到的就是指针的解引用功能。
当然,一般不再用return,也是在函数内直接调用另外一个目标函数,然后把地址作为参数传进去。如下例子。这样做的原因是为了避免在return之后,局部变量的生命中止,原地址被释放。
意义二:指针的存在,相当于提供了一个访问变量的新通道,通过指针,我们可以跨栈区去范围变量,当然也可以访问堆。下面讲malloc和new的时候会用到。
#include<stdio.h>
void function_1(void);
void function_2(int *);
void function_1()
{
int i,temp[10];
for(i=0;i<10;i++)
{
temp[i] = i;
}
function_2(temp);
}
void function_2(int *temp)
{
printf("the value is %d \n",temp[5]);
}
int main()
{
function_1();
return 0;
}
2.3、清空指针
再次强调,指针其实一种存放地址的特殊变量,所以清空指针,是让地址不指向任何值的意思。那么让其指向 N U L L NULL NULL即可。
*point = NULL;
2.4、常量指针和指针常量
2.4.1、常量指针(const int *p;)
名字记忆:
c
o
n
s
t
(
c
o
n
s
t
a
n
t
:
const(constant:
const(constant:常量)
+
i
n
t
∗
p
(
p
o
i
n
t
:
+ int *p(point:
+int∗p(point:指针) = 常量指针。
用法记忆:
c
o
n
s
t
const
const后面的东西不能被修改
−
−
>
∗
p
-->*p
−−>∗p不能被修改
−
−
>
-->
−−>不能通过解引用来改变指针指向的值。
即指针所指向的地址可以变,指针所指向的值也可以改变。但不过通过解引用指针来改变指针指向的值。
eg_1:(正常使用)
int temp = 9;
const int *p;
p = &temp;
printf("%d",*p); //输出结果是9
eg_2:(改变变量的值,进而改变指针指向的值)
int temp = 9;
const int *p = &temp;
temp = 3;
printf("%d",*p); //输出结果是3
eg_3:(改变指针存放的地址)
int temp1=3,temp2=5;
const int *p = &temp1;
p = &temp2;
printf("%d",*p); //输出结果是5
eg_4:(通过解引用改变指针指向的值❌)
int temp = 9;
const int *p;
p = &temp;
*p = 3; //不能通过解引用指针来改变指针所指向的值
printf("%d",*p); //程序报错❌
2.4.2、指针常量(int *const p;)
名字记忆:
i
n
t
∗
p
(
p
o
i
n
t
int *p(point
int∗p(point:指针)
+
c
o
n
s
t
(
c
o
n
s
t
a
n
t
:
+ const(constant:
+const(constant:常量) = 指针常量。
用法记忆:const后面的东西不能被修改---->p不能被修改---->p是一个地址---->地址不能被修改。
即可以改变指针指向的值,也可以通过解引用指针来改变指针指向的值,但就是不能修改指针存放的地址。
eg_1:(正常使用)
int temp = 9;
const int *p;
p = &temp;
printf("%d",*p); //输出结果是9
eg_2:(通过解引用改变指针指向的值)
int temp = 9;
const int *p;
*p = 7;
printf("%d",*p); //输出结果是7
eg_3:(改变变量的值,进而改变指针指向的值)
int temp = 7;
const int *p = &temp;
temp = 9;
printf("%d",*p); //输出结果是9
eg_4:(改变指针存放的地址❌)
int temp1=3,temp2=5;
const int *p = &temp1;
p = &temp2; //不能改变指针存放的地址
printf("%d",*p); //程序报错❌
最后提一句,const来头不简单,const修饰的数据是被丢到只读存储区 ( r e a d o n l y m e m o r y ) (read only memory) (readonlymemory)的。从内存上看, i n t int int和 c o n s t i n t const int constint存放在完全不同的地方。
2.5、C++类中的this
t
h
i
s
this
this是每一个对象(类的实例化)都有的一个隐含参数,它是对象本身的地址,作用域也在仅限于对象自身。
注:声明一个东西,无论是类还是其他变量,它本身的变量不占空间的,只有实例化之后才开始占据内存,才有所谓的地址。
举个例子:
class Car
{
public:
int temp1;
Car(int num){
cout<<"the license plate of this car is:"<<num<<endl;
}
void run(){
int num = this->temp1; //访问对象的temp1属性,当然在这里不加这个this也是可以的。用->是因为this是指针。
}
private:
int temp2;
};
3、malloc and new
3.1、malloc和new的异同
m
a
l
l
o
c
malloc
malloc和
n
e
w
new
new都是用来向堆申请内存空间的函数指令,其返回值都是一个地址。
其中malloc的函数定义是:
v
o
i
d
∗
m
a
l
l
o
c
(
u
n
s
i
g
n
e
d
i
n
t
s
i
z
e
)
void * malloc(unsigned int size)
void∗malloc(unsignedintsize); 可以看出,
m
a
l
l
o
c
malloc
malloc为程序申请了一个
s
i
z
e
size
size字节大小的控制,并返回一个这个空间的首地址。
使用方法:
int *p = malloc(4);
float *p = malloc(4);
...
free(p); //malloc和free是成对出现的,使用完内存,要用free把内存还回去。
new则常用于 C + + C++ C++为类分配内存空间,可以理解为malloc的进化版。new的实现过程可以分为两步,第一步和 m a l l o c malloc malloc一样,向堆申请空间。第二步则是执行构造器,对类进行初始化。
#include <iostream>
using namespace std;
class Car
{
public:
Car(int num){
cout<<"the license plate of this car is:"<<num<<endl;
}
int temp1;
private:
int temp2;
};
int main()
{
Car *mycar = new Car(888);
mycar->temp1 = 5;
//mycar->temp2 = 5; ×,即便指针,也不能访问私有变量
delete(mycar); //delate和new成对出现,也是为了释放已经使用完毕的堆空间,反正内存泄漏。
mycar = NULL; //最好还是清空一下指针
return 0;
}
其中, C a r ∗ m y c a r = n e w C a r ( 888 ) Car *mycar = new Car(888) Car∗mycar=newCar(888)意思是向堆申请一块可以存放这个类所有成员函数和变量的空间,返回这个空间的首地址。那么当然得用指向这个类的指针去接收这个地址了。而由于mycar此时已经是一个指针,那么要去访问类里面的成员变量就要用到’->‘而不是’.'了。
mycar->temp1 等于 (*mycar).temp1
3.2、new的两种用法
在 C + + C++ C++的编程过程中,会产生很多父类以及它们对应的子类。所以用 n e w new new来分配空间的时候也产生了两种选择。
1、父类名 *p1 = new 子类名;
2、子类名 *p2 = new 子类名;
注:子类名 *p = new 父类名; 是错误的❌
在第一种情况中,可以表述为,向堆申请子类的空间,用的却是父类的指针。
p
1
p1
p1可以访问父类
p
u
b
l
i
c
public
public中的任何属性和方法,但不可以访问子类独有的属性和方法。另外,当父类的虚方法在子类中有重载的时候,则以子类为主,否则以父类为主。
在第二种情况中,可以表述为,向堆申请子类的空间,且用的是子类的指针。故
p
2
p2
p2不仅可以访问父类
p
u
b
l
i
c
public
public中的任何属性和方法,也可以访问子类独有的属性和方法。
#include<iostream>
using namespace std;
class Superclass
{
public:
void function_1(void){
cout<<"the father's function 1"<<endl;
}
void function_2(void){
cout<<"the father's function 2"<<endl;
}
virtual void function_3(void){
cout<<"the father's function 3"<<endl;
}
private:
void function_4(void){
cout<<"the father's function 4"<<endl;
}
};
class Subclass:public Superclass
{
public:
void function_1(void){
cout<<"the son's function 1"<<endl;
}
void function_3(void){
cout<<"the son's function 3"<<endl;
}
void function_5(void){
cout<<"the son's function 5"<<endl;
}
};
int main()
{
Superclass *p1 = new Subclass;
Subclass *p2 = new Subclass;
p1->function_1(); //print "the father's function 1"
p1->function_2(); //print "the father's function 2"
p1->function_3(); //print "the son's function 3"
// p1->function_4(); //wrong, the function doesn't exist in the subclass space❌
// p1->function_5(); //wrong, the pointor of the superclass can't access method unique to the subclass❌
p2->function_1(); //print "the son's function 1"
// p2->function_2(); //wrong,there is not function_2 in the subclass❌
p2->function_3(); //print "the son's function 3"
// p2->function_4(); //wrong, subclass can't access the private function of superclass❌
p2->function_5(); //print "the son's function 5"
}
4、命名空间
4.1、创建命名空间
一般把命名空间放在头文件里,不同的命名空间也是使用不同的栈区,这使得它们即使有相同名字的变量也毫不冲突。
在以下的这个例子里,假设我们创建了一个myspace.h文件,然把命名空间到定义放在.h文件中。
#ifndef MY_SPACE // ifndef == if no define。防止多次引用头文件的时候,重复执行以下代码
#define MY_SPACE
namespace myspace
{
class test_1; //可以在花括号外再定义类,在这里只是声明一下test_1属于myspace命名空间里的类。
void test_2(void);
void test_2(void) //函数必须在花括号内定义好
{
cout<<"test..."<<endl;
}
}
class test_1
{
public:
test_1(void){
cout<<"i am the construct of the test_1"<<endl;
}
void test_method(void);
};
#endif
4.2、使用命名空间
#include "myspace.h" //引用自己创建的头文件用“”号,引用系统的文件的时候用<>号。
using namespace myspace;
用了以上这句代码,就可以在该源文件中使用myspace里的所有函数和类。这其实和之前用到的using namespace std是一样的原理。但如果怕不同命名空间在这个文件中出现同名函数或同名变量冲突,可以不使用using namespace …,在需要用到该空间函数或变量的时候再使用以下语句。
myspace::函数or变量
5、编译程序的四个步骤
5.1、预处理(Pre-process)
预处理阶段会将所有必要的头文件拉取进来,并展开程序中的宏定义(# d e f i n e define define)、判别代码的存留(# i f d e f ifdef ifdef,# e n d i f . . . endif... endif...)、删除代码中的注释等等,最后留下需要执行的代码内容,生成 . i .i .i文件。
5.2、编译(Compile)
检查程序的语法和规范等问题,并把代码转化成汇编语言。同时在这个步骤,编译器会为程序分配好内存空间,并处理好常量(const),最后生成 . s .s .s文件。
5.3、汇编(Assembly)
将 . s .s .s文件中的汇编码转化为机器能读懂的二进制指令,生成 . o .o .o文件(也称二进制文件)。
5.4、链接(Link)
将多个二进制文件链接起来,形成一个可执行文件。比如在Windows系统下的可执行文件是
.
e
x
e
.exe
.exe作为后缀的。