• C++学习笔记二(堆栈、指针、命名空间、编译步骤)


    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
    using namespace std;
    
    class Car
    {
    public:
    	Car(void){
    		cout<<"this is a class of Car"<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这里有几个点需要注意:
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再通过以下几个例子可以进一步了解指针本质。

    错误示例:
    	int *p;  
    	*p = 9;
    
    • 1
    • 2
    • 3

    错误原因:这里 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    正确原因:通过 p = p= p=& a a a;这句代码让p指针存放a变量的地址,那么无论是直接修改a,还是通过地址索引a再修改之,结果都是一样的。
    此外,由于在32为计算机,地址则占4个字节,在64位计算机中,地址占6个字节或8个字节的缘故,指针的大小也是其对应地址所占的空间。

    2.2、指针的意义

    指针可以说是C语言的灵魂,即便在C++当中也发挥着巨大的作用。以下对于指针的意义仅是我个人学习过程中的所感所获。
    意义一:在大部分程序语言中,函数的返回值一般分为两种情况,一种是返回一个值,另一种则是返回一个地址。我们称之为传值传址。而传址的目的,还是为了获得目标值,所以此时用到的就是指针的解引用功能。
    当然,一般不再用return,也是在函数内直接调用另外一个目标函数,然后把地址作为参数传进去。如下例子。这样做的原因是为了避免在return之后,局部变量的生命中止,原地址被释放。
    意义二:指针的存在,相当于提供了一个访问变量的新通道,通过指针,我们可以跨栈区去范围变量,当然也可以访问堆。下面讲malloc和new的时候会用到。

    #include
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.3、清空指针

    再次强调,指针其实一种存放地址的特殊变量,所以清空指针,是让地址不指向任何值的意思。那么让其指向 N U L L NULL NULL即可。

    *point = NULL;
    
    • 1

    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: +intp(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);  //程序报错❌
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.4.2、指针常量(int *const p;)

    名字记忆: i n t ∗ p ( p o i n t int *p(point intp(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);        //程序报错❌
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    最后提一句,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:"<temp1;    //访问对象的temp1属性,当然在这里不加这个this也是可以的。用->是因为this是指针。
    	}
    private:
    	int temp2;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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) voidmalloc(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把内存还回去。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    new则常用于 C + + C++ C++为类分配内存空间,可以理解为malloc的进化版。new的实现过程可以分为两步,第一步和 m a l l o c malloc malloc一样,向堆申请空间。第二步则是执行构造器,对类进行初始化。

    #include 
    
    using namespace std;
    
    
    class Car
    {
    public:
    	Car(int num){
    		cout<<"the license plate of this car is:"<temp1 = 5;	
    	//mycar->temp2 = 5;  ×,即便指针,也不能访问私有变量  
    	delete(mycar);     //delate和new成对出现,也是为了释放已经使用完毕的堆空间,反正内存泄漏。
    	mycar = NULL; //最好还是清空一下指针
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    其中, C a r ∗ m y c a r = n e w C a r ( 888 ) Car *mycar = new Car(888) Carmycar=newCar(888)意思是向堆申请一块可以存放这个类所有成员函数和变量的空间,返回这个空间的首地址。那么当然得用指向这个类的指针去接收这个地址了。而由于mycar此时已经是一个指针,那么要去访问类里面的成员变量就要用到’->‘而不是’.'了。

    mycar->temp1 等于 (*mycar).temp1
    
    • 1

    3.2、new的两种用法

    C + + C++ C++的编程过程中,会产生很多父类以及它们对应的子类。所以用 n e w new new来分配空间的时候也产生了两种选择。

    1、父类名 *p1 = new 子类名;    
    2、子类名 *p2 = new 子类名;
    
    注:子类名 *p = new 父类名;    是错误的❌
    
    • 1
    • 2
    • 3
    • 4

    在第一种情况中,可以表述为,向堆申请子类的空间,用的却是父类的指针。 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
    using namespace std;
    
    class Superclass
    {
    public:
    	void function_1(void){
    		cout<<"the father's function 1"<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"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    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..."<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4.2、使用命名空间

    #include "myspace.h"  //引用自己创建的头文件用“”号,引用系统的文件的时候用<>号。
    using namespace myspace;
    
    • 1
    • 2

    用了以上这句代码,就可以在该源文件中使用myspace里的所有函数和类。这其实和之前用到的using namespace std是一样的原理。但如果怕不同命名空间在这个文件中出现同名函数或同名变量冲突,可以不使用using namespace …,在需要用到该空间函数或变量的时候再使用以下语句。

    myspace::函数or变量
    
    • 1

    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作为后缀的。
    在这里插入图片描述

  • 相关阅读:
    Json Gson类型转换
    华为机试真题 C++ 实现【流水线调度】
    centos7.6升级openssh9.3p1,openssl1.1.1t
    mybatis-plus使用小結
    跨平台编译QCA、安装QCA(Windows、Linux、MacOS环境下编译与安装)
    华为云云耀云服务器L实例评测|使用Portainer部署showdoc文档工具
    文件存储服务器调研
    B. Jellyfish and Game-Codeforces Round 902 (Div. 2)
    WPF 控件专题 TreeView控件详解
    Javascript 数字精度丢失的问题,如何解决?
  • 原文地址:https://blog.csdn.net/weixin_45885074/article/details/132768014