当前位置:   article > 正文

C++基础知识1[main函数, 结构体内存对齐, 指针和引用的区别, 堆和栈, 指针类型, 虚函数表, new / delete 与 malloc / free, 宏定义和函数]_main函数创建的整型变量是栈内存吗

main函数创建的整型变量是栈内存吗

本文内容主要基于公众号拓跋阿秀整理

1. main函数执行之前之后的代码可能是什么?

main函数执行之前,主要就是初始化系统相关资源:

  • 设置栈指针
  • 初始化静态 static 变量和 global 全局变量,即 .data 段的内容
  • 将未初始化部分的全局变量赋初值:数值型 short , int , long 等为 0 , bool 为 FALSE ,指针为 NULL 等等,即 .bss 段的内容
  • 全局对象初始化,在 main 之前调用构造函数,这是可能会执行前的一些代码
  • 将main函数的参数 argc , argv 等传递给 main 函数,然后才真正运行 main 函数
  • __attribute__((constructor))

main函数执行之后:

  • 全局对象的析构函数会在main函数之后执行;
  • 可以用 atexit 注册一个函数,它会在main 之后执行;
  • __attribute__((destructor))

2. 结构体内存对齐问题

为什么要内存对齐?

经过内存对齐之后,CPU的内存访问速度大大提升;
内存空间按照byte划分,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
数据项仅仅能存储在地址是数据项大小的整数倍的内存位置上

对齐规则?

3. 指针和引用的区别

  1. 指针和引用的定义和性质区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;
int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

(3)可以有const指针,但是没有const引用;
常量指针:指向常量的指针,在指针定义语句的类型前加const,表示指向的对象是常量。
定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性。
常量指针定义const int* pointer=&a告诉编译器,*pointer是常量,不能将*pointer作为左值进行操作。
常量引用:指向常量的引用,在引用定义语句的类型前加const(const int& ref = i),表示指向的对象是常量。也跟指针一样不能利用引用对指向的变量进行重新赋值操作。

指针常量在指针定义语句的指针名前加const,表示指针本身是常量。在定义指针常量时必须初始化!而这是引用天生具来的属性,不用再引用指针定义语句的引用名前加const。

指针常量定义int* const pointer=&b告诉编译器,pointer是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pointer可以修改。

总结:有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身与天俱来就是常量,即不可以改变指向。

(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(7)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

(8)指针和引用的自增(++)运算意义不一样;
对引用变量ref的++操作是直接反应到指向的变量之上,对引用变量ref重新赋值"ref=j",并不会改变ref的指向,它仍然指向的是i,而不是j。理所当然,这时对ref进行++操作不会影响到j。

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

  1. 指针和引用作为函数参数进行传递时的区别。

(1)指针作为参数进行传递: 改变地址不影响实参值, 改变指针指向的地址存储的值,可对实参进行改变。
(2)将引用作为函数的参数进行传递: 实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

#include<iostream>
#include<stdlib.h>
using namespace std;


void un_swap_int(int *a,int *b)
{
    int *temp=a;//改变指针指向,但是没改变指针的所指的内容
    a=b;
    b=temp;
}
void swap_int(int *a,int *b)
{
    int temp=*a;//改变指针指向,但是没改变指针的所指的内容
    *a=*b;
    *b=temp;
}

void test(int *p)
{
    int a=1;
    p=&a;//改变指针指向,但是没改变指针的所指的内容
    cout<<"p = " << p <<" "<<"*p = " << *p<<endl<<endl;
}
void test2(int *&p)//引用作为函数参数进行传递时,实质上传递的是实参本身
{
    int a=1;
    p=&a;
    cout<<p<<" "<<*p<<endl<<endl;
}

int main(void)
{
    int a=1,b=2;
    int *p=NULL;
    un_swap_int(&a,&b);
    cout<<"a = " << a<<"  b = "<<b<<endl<<endl;//a = 1  b = 2
    test(p);//p = 0x7fffae2df244 *p = 1
    if(p==NULL)
        cout<<"指针p为NULL"<<endl<<endl;//指针p为NULL
}
  • 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

C++中指针和引用的区别
c++中,引用和指针的区别是什么?

4. 堆和栈的区别

  • 申请方式不同
    栈由系统自动分配。
    堆是自己申请和释放的。
  • 申请大小限制不同。
    栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改。
    堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同。
    栈由系统分配,速度快,不会有碎片。
    堆由程序员分配,速度慢,且会有碎片。
  • 栈空间默认是4M, 堆区一般是 1G - 4G

形象的比喻
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切
菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
C++中堆(heap)和栈(stack)的区别

管理方式堆中资源由程序员控制(容易产生memory leak)栈资源由编译器自动管理,无需手工控制
内存管理机制系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中)只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了)
空间大小堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置)
碎片问题对于堆,频繁的new/delete会造成大量碎片,使程序效率降低对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。
生长方向堆向上,向高地址方向增长。栈向下,向低地址方向增长。
分配方式堆都是动态分配(没有静态分配的堆)栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
分配效率堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令。

5. 区别以下指针类型?

int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)

  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

6. 基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

首先整理一下虚函数表的特征:

  • 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成;
  • 虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段;
  • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中。

根据以上特征,**虚函数表类似于类中静态成员变量.静态成员变量也是全局共享,**大小确定,因此最有可能存在全局数据区,测试结果显示:

虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别

由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
虚函数表存放在哪里
一般分为五个区域: 栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区

C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

7. new / delete 与 malloc / free的异同

相同点

  • 都可用于内存的动态申请和释放

不同点

  • 前者是C++运算符,后者是C/C++语言标准库函数
  • new自动计算要分配的空间大小,malloc需要手工计算
  • new是类型安全的,malloc不是。例如:

int *p = new float[2]; //编译错误
int p = (int)malloc(2 * sizeof(double));//编译无错误

  • new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用
  • 后者需要库文件支持,前者不用
  • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

8. new和delete是如何实现的?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针;
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。

9. malloc和new的区别?

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,并且支持重载。
  • malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
  • malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

9.1 delete和delete[]区别?(补充)

  • delete只会调用一次析构函数。
  • delete[]会调用数组中每个元素的析构函数。

10. 宏定义和函数有何区别?

  • 宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  • 宏定义不要在最后加分号。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/807787
推荐阅读
相关标签
  

闽ICP备14008679号