数据类型

基本数据类型

变量所占大小

X64X86
char1字节1字节
short2字节2字节
int4字节4字节
long8字节4字节
long long8字节8字节
float4字节4字节
double8字节8字节
long double 通常16字节通常8字节

        char

        分为三类型:

        1.char:标准类型

        2.unsigned char:无符号char(0~255)

        3.signed char:有符号char(-128~127)

运算符优先级

优先级运算符
1

()

[]

.

->

2

单目运算符

++(前置)

--(前置)

+

-

!

~

*

&

3

乘除运算符

*

/

%

4

加减运算符

+

-

5

移位运算符

<<

>>

6

关系运算符

<

<=

>=

>

7

相等运算符

==

!=

8

按位与运算符

&

9

按位异或运算符

^

10

按位或运算符

!

11

逻辑与运算符

&&

12

逻辑或运算符

||

13

条件运算符

?  :

14

赋值运算符

=

+=

-=

*=

/=

%=

<<=

>>=

&=

^=

|=

15

逗号运算符

构造类型

        数组

        1.一维数组

                字符串/字符数组

char arr1[] = "hello"; 字符串:结尾有'\0'终止符,arr1占6字节
char arr2[] = {'H', 'e', 'l', 'l', 'o'}; 字符数组:结尾无'\0',arr2占5字节
'\0'影响printf等输出,输出结果为'\0'之前

                字符串的申明方式

                1.静态分配,编译时在栈上分配好内存

                2.字符串常量

                3.动态分配内存(malloc和new)

                4.自动分配,在局部函数中

                操作函数

size_t strlen(const char *str) 返回字符串长度,不包含'\0'
char *strcpy(char *dest, char *src) 把src复制给dest
char *strncpy(char *dest, const char *src, size_t n) 把src复制给dest前n个字符
char *strcat(char *dest, const char *src) 把src添加到dest的结尾
char *strncat(char *dest, const char *src, size_t n) 把src前n个字符添加到dest结尾
int strcmp(const char *str1, const char *str2) 比较字符串是否相同,成功返回0,失败返回不同个数
int strncmp(const char *str1, const char *str2, size_t n) 比较前n个字符串,返回与strcmp相同
char *strchr(const char *str, int c) 返回c第一次在str出现的地址
char *strrchr(const char *str, int c) 返回c最后一次在str出现的地址
char *strstr(const char *haystack, const char *needle) 返回needle在haystack第一次出现的地址

                地址

char arr[10] = "Hello";
arr与&arr[0]所表示是一样的
&arr是整个数组的地址,类型是char (*)[10],表示10个指向char元素的指针

                指针数组与数组指针

指针数组,包含3个char的指针
char *ptr_arry[3]
数组指针,指向一个包含3个char的数组
char (*arry_ptr)[3]
        2.二维数组

                初始化

1.没什么好说的
int array[3][4];
2.编译器自动分配
int array[][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};
3.分配动态内存
int rows = 3;
int cols = 4;
int **array = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
    array[i] = (int *)malloc(cols * sizeof(int));
}
4.使用stl容器
int rows = 3;
int cols = 4;
std::vector<std::vector<int>> array(rows, std::vector<int>(cols));

                地址 

                &a[1],&a[0]==a;一维数组地址

                &a[0][1],&a[0][0] == a[0];元素地址

                &a;二维数组地址

        结构体

                声明

1.默认声明
struct Point {
    int x;
    int y;
};
C++的结构体class,为兼容c,保留了struct
2.定义并声明实例
struct Point {
    int x;
    int y;
} p1, p2;
3.结构体别名
typedef struct {
    int x;
    int y;
} Point;
Point p1; 
C++默认声明,创建实例时不需要声明结构体struct或class

                结构体大小

struct Example {
    char a;    // 1 byte
    int b;     // 4 bytes
    char c;    // 1 byte
};

                以上面为例,变量存储地址为最大变量占字节大小的倍数,设从0x00开始,char占1个字节,地址在0x00,b占4字节,因为地址要是自身字节大小的整数倍,所以地址在0x04;c占1个字节,地址在0x08,所以Example结构体的大小为12字节。

                其中,c特有的位域,变量:位数,用于修改变量大小,节省内存空间。计算方式为同类型变量合并,不足该变量字节大小的部分自动填充,不同类型的变量按结构体的计算方式存入地址。

                链表

                单链表:简单,节点只包含指向下一个节点的指针。

                双向链表:支持双向遍历,节点包含指向前一个和下一个节点的指针。

                循环链表:尾节点指向头节点,形成一个环,适合需要循环访问的情况。

                C++中class与struct区别

                (1)相同点

                都能拥有成员函数,公有和私有部分;class可以实现的struct也可以实现。

                (2)不同点

                struct默认公有,class默认私有;struct默认公有继承,class默认私有继承。 

                (3)C++的struct与C的区别

                C语言中struct是自定义数据类型;C++中是抽象数据类型,支持成员函数的定义(C++中可以继承和实现多态)

                C语言中struct没有访问权限设置,成员只能是变量,但可以存入函数地址,数据不能被隐藏。

                C++设置了访问权限,功能与class一样,默认是public访问。

                C语言声明实例时必须在前面加struct,除非定义结构体时使用typedef。C++不需要,结构体struct在C++中被当作特例。

        联合体

                联合体声明     

union Data {
    int i;
    float f;
    char str[20];
};

                联合体大小计算

                联合体的大小是其最大的数据成员大小的整数倍再满足编译器的内存对齐要求的最小倍数。以上面为例,联合体最大成员时char str[20],则联合体大小为20字节,若内存对齐要求为4字节,则不变;若为8字节,则填充4字节为24字节。

                判断大小端

#include <iostream>
union data{
    int a;
    char b;
}

void main()
{
    data udata;
    udata.a = 0x12345678;
    if(udata.b == 0x78)
        printf("Little-Endian\n");
    else if(udata.b == 0x12)
        printf("Big-Endian\n");
    else
        printf("Unkonwn Endian\n");
}

        指针 

         内存的申请与释放
                C++中的new
分配内存,构造一个整数
int* ptr = new int;
分配内存,构造函数参数进行构造
MyClass* ptr = new MyClass(arg1, arg2);
分配数组内存
MyClass* array = new MyClass[5];
或者memset设置内存初始值
使用std::nothrow,new失败时返回null
                malloc
type p = (type)malloc(分配大小)
分配失败时返回null
                malloc与new的异同

                相同点

                都可以动态申请内存

                不同点

                new是C++操作符,支持重载,还会调用构造函数;malloc是C/C++的标准函数。

                new是类型安全的,malloc不安全。

                new返回具体指针,malloc返回void型指针,需要类型转换。

                new自动计算分配内存大小,malloc需要手动计算。

                new是封装了malloc。

                C++中的delete

                释放内存 

释放单个对象的内存
MyClass* obj = new MyClass(); // 用 new 分配对象
delete obj; // 释放分配的对象内存
释放数组的内存
MyClass* array = new MyClass[10]; // 用 new[] 分配对象数组
delete[] array; // 释放分配的数组内存
一个new对应一个delete
释放内存后需要将指针指向空
delete null是安全的
                free

                释放内存

int *array = (int *)malloc(10 * sizeof(int));
free(array);
一个malloc对应一个free
内存释放后将指针指向null,避免产生野指针
free(NULL)会崩溃
               new和delete是如何实现的

                new的实现过程:对简单的函数直接使用operator new函数;对复杂的数据结构调用operator new函数,分配足够大的原始为类型化的内存,运行该类型的构造函数并传入初始值,最后返回该对象的指针。

                delete的实现过程:简单数据类型直接调用free;对复杂的数据结构对指针指向的对象运行析构函数,再用operator delete函数释放对象所使用内存。

                new[]一个数组对象,需要知道数组的长度,会多分配4个字节,实际的数组所占内存为p-4;delete[]操作会取出这个数,知道要调用多少次析构函数。

                malloc和free的实现

                这两个函数是由brk、mmp和munmap这些系统调用实现的。

                brk是将堆顶的指针向高位移动,获得新的内存空间。mmap是在进程的虚拟地址空间(堆和栈中间,称为文件映射区)中找到一块空闲的内存块。这两种都是分配虚拟内存,没有分配实际的物理内存。在第一次访问已分配的虚拟地址空间,发生缺页中断(当程序访问的虚拟内存页面不存在物理内存时,会触发缺页中断),操作系统分配物理内存,建立虚拟内存与物理内存的映射关系。

                malloc分配内存时,当分配内存小于128k,则使用brk在堆顶向高地址移动指针;当分配的内存大于128k时,使用mmap在虚拟地址空间寻找一块空闲内存。当最高地址空间的空闲内存超过128K,则会执行内存紧缩。    

               被free回收的内存是立即还给操作系统了吗

                不是的,被free回收的内存会用双链表ptmalloc保存,当下次申请内存的时候就尝试再内存中寻找合适的返回,避免反复的系统调用,同时tpmalloc也会尝试合并小块内存,防止产生过多内存碎片。

                calloc与realloc

                calloc省去了人为空间计算,calloc申请的空间的初始值是0;realloc给动态分配的空间分配额外的空间。

                深拷贝和浅拷贝

                浅拷贝

class a{
    public:
        char *data;
        a(const char* str){
            data = new char[strlen(str)+1];
            strcpy(data, str);
        }
        a(const a &other) : data(other.data) {} //浅拷贝构造函数
        a();
        ~a();
}

int main()
{
    a a1("hello world!");
    a a2 = a1;
    return 0;
}

               a1直接赋值给a2,实际a2使用的内存与a1使用的是同一块,a1内存被回收后,a2会访问无效内存,发生未定义行为。

                深拷贝

class a{
    public:
        char *data;
        a(const char* str){
            data = new char[strlen(str)+1];
            strcpy(data, str);
        }
        a(const a &other){
            data = new char[strlen(other.data)+1];
            strcpy(data, other.data);
        }//深拷贝构造函数
        a();
        ~a();
}

int main()
{
    a a1("hello world!");
    a a2 = a1;
    return 0;
}

                a2不仅复制了a1的值,还分配了独立内存 。

         指针类型
                数组指针与指针数组

                数组指针:数组是指针;指针数组:成员是指针。

                二级指针

                一级指针指向某数据类型的内存地址,二级指针指向一级指针的内存地址。

                函数指针
int (*funcPtr)(int, int);

                它用于指向函数的内存地址。

                指针大小

                32位一般是4字节,64位一般是8字节。地址+1是加了一个类型的大小。

                *的三种作用

                (1)解引用运算符:*指针变量,表示地址操作符,取内容

                (2)指针申明:表示指针变量

                (3)运算:表示乘

                指针和数组的区别

                1.数组在内存中是连续的,开辟一块连续的内存空间。数组内存大小为sizeof(数组名),数组长度为sizeof(数组名)/sizeof(数组元素数据类型)。

                2.sizeof(指针),得到的是指针所占内存大小,而不是指向的内存大小。

                3.为了简化对数组的支持,实际上利用指针实现了对数组的支持,通过数组首地址加偏移量进行索引。

                4.向函数传递参数,如果实参是数组,那接收的形参是指针,传递过去的不是整个数组,二是数组的首地址,提高了效率。

                5.数组的原地址也就是首地址是固定的,指针的原地址是不固定的。

        C++引用变量
                声明
int b = 4;
int &a = b;

                是对已存在变量的别名。

                常量引用
const int &value

                值不允许修改。

                数组引用与指针一样。

                指针的引用
int *a = new int;
int *(&b) = a;
                引用与函数

                不要引用局部变量。

                引用与指针的区别

                (1)引用声明的初始化,指针不用马上初始化

                (2)引用不能指向空,指针可以

                (3)引用初始化后不能指向其他变量,指针可以

                (4)引用效率高

                (5)引用更安全,指针可以偏移

                (6)指针更灵活,直接操作地址;指针更通用,C/C++都可以。

        在main执行前和之后执行的代码可能输什么

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

                设置栈指针

                初始化静态static变量和global全局变量,即.data段

                将未初始化部分的全局变量赋初值:short、int、long等为0,bool为false,指针为NULL等等,即.bss段。

                全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码

                将main函数的参数argc、argv等传入main函数,然后真正运行main

                __attritube__((constructor))

                main函数执行后:

                全局对象的析构函数会在main函数之后执行

                可以用atexit注册一个函数,它会在main之后执行

                __attritube__((destructor))

        修饰符

                static的用法和作用

                1.隐藏

                2.保持变量内容的持久,即改变变量的生命周期。将static修饰的变量存入静态数据区(全局区)。

                3.默认初始化为0。

               4.在C++中类成员声明static:

                (1)函数体内static变量的作用范围为该函数体,内存只分配一次。

                (2)在模块内的static全局变量只能被该模块的函数访问。

                (3)模块内的static函数只能被该模块的函数调用。

                (4)类中的static成员变量属于类所有,对类的所有对象只有一份拷贝,该变量的初始化要在类外。

                (5)类中的static成员函数属于类所有,这个函数没有this指针,只能访问类中的静态成员变量。

                类内:               

                (6)static类对象要在类外初始化,因为static修饰的变量先类存在。

                (7)static修饰的类成员属于类不属于对象,所以没有this指针,this指针是指向本成员的指针,所以无法访问非static的类成员。

                (8)static成员函数不能被virtual修饰,static成员不属于任何对象和实例,virtual加上没有任何意义;静态成员没有this指针,虚函数的实现是对每个对象分配vptr指针,而vptr指针由this指针调用,所以不能为virtual。虚函数的调用关系:this->vptr->ctable->virtual function。

                静态变量什么时候初始化

                1.初始化只有一次,在主程序前编译器已经分配好内存。

                2.静态局部变量与全局变量一样存在全局区。在C中,初始化发生在执行代码前,编译阶段分配内存后,就会初始化,所以C语言中无法用变量初始化静态局部变量;程序结束,变量所处的全局内存会被回收。

                3.在C++中,初始化在执行相关代码时,主要时C++引入对象后,要进行初始化必须要用构造函数或析构函数,构造和析构函数一般需要执行相关的程序,而非简单分配内存。所以C++规定在首次使用到全局或静态变量时进行构造,并通过atexit()管理。在程序结束时,根据构造的顺序反方向析构,所以C++中的静态局部变量可以用变量初始化。

                指针与const的用法

int val = 10;
指针常量
int* const a = &val;
常量指针
const int *b = &val;

                指针常量是指指针是常量,即指针指向的地址不可变,但指向地址的内容可变。

                常量指针是指指针指向的内容不可变,地址可变。

                volatile、mutable和explicit关键字用法

                volatile

                定义变量的值是易变的,防止被编译器优化,声明后系统总是重新从内存中读取数据。

                多线程下作用是防止优化编译器把内存装入CPU寄存器。

                注意:可以把非volatile int赋给volatile int,但不能非volatile对象赋给volatile对象;用户定义的类型也可以用volatile修饰;volatile的类只能访问它的接口子集,一个由类控制者的子集,用户只能通过const_cast来获得对类型接口的完全访问。

                volatile指针

volatile指针
volatile char *p;
指针volatile
char* volatile p;
与常量指针和指针常量类似

                volatile指针:指针指向的内容是易变的。

                指针volatile:指针指向的地址是易变的。

                mutable

                修饰对象将永远可以修改。在const函数里修改与类状态无关的数据成员。

class person{
    mutable int a;
    public:
        void add() const
        {
            a = 10; 
        }
};

int main()
{
    const person p = person();
    p.add();
    p.a = 40;
    return 0;
}
                explicit

                explicit修饰构造函数,那么构造函数不能用隐式。explicit修饰转换操作符,必须显示转换。

#include <iostream>

class MyClass {
public:
    explicit MyClass(int x) : value(x) {}  // `explicit` 构造函数
    explicit operator int() const { return value; }

    int getValue() const { return value; }

private:
    int value;
};

void printValue(const MyClass& obj) {
    std::cout << "Value: " << obj.getValue() << std::endl;
}

int main() {
    MyClass obj1(10);  // 合法:显式调用构造函数
    printValue(obj1);  // 合法:传递 `MyClass` 对象
    int x = static_cats<int>(obj) //合法: 显示数据类型转换

    // MyClass obj2 = 20;  // 错误:隐式转换被禁用

    // printValue(30);  // 错误:无法将 `int` 隐式转换为 `MyClass`

    return 0;
}

        final与override

                override

                指定该函数为父类的虚函数重写

class A{
    virtual void foo();
}

class B: public A{
    virtual void fo0() override;//错误,函数一定是继承A的,找不到就报错
    virtual void f00(); //正确
    void foo(); //正确
    virtual void foo(); //正确
    void foo() override(); //正确
}
                final

                不希望某个虚函数被继承或重写

class Base
{
    virtual void foo();
}
class A: public Base
{
    void foo() final;
}
class B final : A
{
    void foo() override; //错位,foo不能被重写
}
class C: B //错误,B不能被继承
{
}

        define与const的区别

        编译阶段

        define在编译的预处理阶段起作用,const在编译、链接的时候起作用。

        安全性

        define只作替换,不做类型检查和计算,易产生错误;const常量有数据类型,编译器可以对其进行类型安全检查

        内存占用

        define只是将宏名称进行替换,在内存中会产生多份相同的备份;const只有一份备份,可以执行常量折叠,将复杂的表达式计算出结果放入常量表。

        总结

        1.宏替换发生在预处理阶段,属于文本插入;const作用发生于编译、链接过程;

        2.宏不检查类型,const会检查数据类型;

        3.宏定义的数据没有分配内存空间,只是插入替换;const定义的变量只是值不能改变,但要分配内存空间;

        4.defien预处理后占用代码段空间,const占用常量段空间;

        5.const不能重定义,而define可以通过undef取消某个符号,进行重新定义;

        6.define可以防止文件重复引用;

        define和typedef的区别

        1.执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;define是宏定义,发生在预处理阶段。

        2.功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用。define不只是可以为类型取别名。还可以定义常量、变量、编译开关等。

        3.作用域不同,define没有作用域限制,只要是之前定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

        define和inline的区别

        1.define是关键字,inline是函数;

        2.define在预处理阶段替换文本,inline在编译阶段替换;

        3.inline有类型检查,比define安全;

        顶层const与底层const

        顶层const:指const修饰的本身是一个常量,无法修改,在*号右边。

        底层const:指const修饰的变量所指向的对象是一个常量,在*号左边。

C++类

        类和对象

        类

        定义:具有相同属性和行为的对象的集合。

        类的实例

        类声明

class A
{
    public:
        
        void show()
        {
            cout<<a<<endl;
        }
    private:
        int a;
    protected:

};

struct B
{
    public:
    
    private:

    protected:

}

        声明对象

person p;
person *p = new person;

        访问修饰符

                关键字

                private:类内可见,class内不写访问修饰符,默认private;

                public:类外可见,struct默认public;

                protected:类内及子类可见;

                友元
               友元函数
class A
{
    private:
        int b;
    public:
        A() : b(5) {}
        friend void show(A &a);
}

void show(A &a)
{
    cout<<a.b<<endl;
}

int main()
{
    A a;
    show(a);
    return 0;
}
                友元类
class A
{
    private:
        int val;
    public:
        A() : val(20) {}
        friend class FriendA;
}

class FriendA
{
    public:
        void show(A &a) const
        {
            cout<<a.val<<endl;
        }
}

int main()
{
    A a;
    FriendA fa;
    fa.shwo(a);
    return 0;
}

        使用友元可以访问类的public,private和protected,继承可以访问public和protected。

                特点

                不受访问修饰符的影响;可以有多个友元;破坏了类的封装性,不是迫不得已,不要使用。

                使用友元类时注意:

                1.友元关系不能被继承。

                2.友元关系时单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

                3.友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。

        函数成员

                构造函数
                产生

                普通成员不能在类内直接赋值,因为只有对象的创建的时候才会分配空间,构造函数就是对数据成员赋值。

                形式

                类名(参数列表){}

class A
{
    public:
        A() {}
        A(int b) {val = b;}
        A(int b, string str) {s = str;val = b;}
    private:
        string s;
        int val; 
}
                调用

                声明栈区对象时若没有特指构造函数则会调用默认构造函数;声明堆区对象不会调用构造函数,是在new空间的时候调用。

                类型

                (1)默认构造函数

                什么都不做,即为空的。只要宏观声明的构造函数,默认的就没有了。

                (2)无参数

                (3)有参数构造函数

                通过对象传递赋值;可以指定默认值;

                (4)多个构造函数构成重载

class A
{
    public:
        A() : val(10) {}
        A(int b) : val(b) {}
    private:
        int val;
};
               初始化列表

                形式

                构造函数后加冒号。

class A
{
    public:
        A() : val(10) {}
    private:
        int val;
};

                作用

                和构造函数的区别:基本数据类型都行,引用与const必须用初始化列表。

                执行顺序

                在构造函数之前。

                数组和结构体初始化

                数组通常设置全元素为0的方法,如memset或for循环;结构体直接赋值。

                析构函数
                作用

                清理工作:new一个对象,析构函数可以释放掉。

                形式

                ~类名()       只有一个,没有参数,没有重载。

                调用

                对象生命周期结束时自动调用:局部变量在函数结束时结束,临时对象在作用域内,指针对象在delete时结束。

                new、malloc、delete、free区别

                new自动调用构造函数,malloc不行;delete自动调用析构函数,free不行。

                常函数
                形式

                析构与构造函数不能是常函数。

void fun() const {......}
                特点

                可以使用数据成员,但不能修改;常对象只能调用常函数,不能调用普通函数;常函数的this指针类型是const 类名*;

                static
                使用方式

                对象调用;类名作用域。

                静态成员

                类外初始化,初始化时不加static。

                静态常量数据成员可以直接在类内初始化

                静态成员函数

                没有this指针,不能被virtual修饰;不能调用普通成员,可以调用静态成员;是类的属性,不是对象,所有对象共享一个;

                拷贝构造/赋值构造
                形式
class A
{
    public:
        A(const A &a);
}
                何时调用

                (1)新建一个对象

MyClass mc;
MyClass mc1(mc);
MyClass mc2 = mc;
MyClass mc3 = MyClass(mc);
MyClass *mc4 = new MyClass(mc);

                (2)当程序生成对象副本

                函数参数传递对象的值;函数返回对象。

                功能

                默认的复制构造函数,逐个复制非静态成员的值,复制的是成员的值,又叫浅拷贝

                深拷贝

                指针成员不能直接赋值,要用内存拷贝memcpy,strcpyd等。

                解决拷贝构造引发的指针成员二次释放崩溃问题

                深拷贝、传地址、传引用。

                为什么拷贝构造函数必须传引用而不能传值

                1.构造拷贝函数的作用是复制对象,在使用这个对象的实例来初始化这个对象的一个新的实例

                2.参数传递的过程中,将地址传递和值传递统一起来;值传递:对于内置数据类型的传递时,直接赋值拷贝给形参,对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);引用传递:无论对内置类型还是类类型,传递引用或指针最终都是传递的地址,因此不会像类类型值传递需要调用构造函数。

                值传递会先对局部变量类的实例调用拷贝构造函数来初始化实例,会产生无限递归,栈溢出。

                如何禁止程序自动生成拷贝构造函数

                1.为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。

                2.类的成员函数和friend函数还是可以调用private函数,如果这个private函数只申明不定义,则会产生一个连接错误。

                3.针对两种情况,可以定义一个base类,在base类中将拷贝构造函数和拷贝复制函数设置为private,那么派生类中编译器将不会生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关操作。

                内联函数
                常规函数调用过程

                调用时,根据函数地址跳到代码空间执行代码,执行完再跳转到调用位置;综合看:来回跳跃+记录跳跃位置,需要消耗一定的系统开销。

                内联函数

                inline:函数声明要加inline,函数定义要加inline,只写声明位置没用。

                作用:用相应的代码代替调用,比常函数稍快,代价是占用更多内存。

                常规函数的内联函数如何选择

                特点:时间和空间看性价比和实际需要。

                实际:函数代码少,流程直接,调用频繁的选择内联函数。

                编译器智能:程序员请求把函数作为内联函数,编译器不一定答应;如函数体过大,递归不能是内联函数,效果就是写了等于没写。

                内联函数比宏功能更强

                类型安全:内联函数有类型检查。

                调试友好:内联函数在调试时能有更多信息。

                类与内联函数

                类内定义的函数都是内联函数,不写的就是隐式inline,写就是显示inline,写不写都行。

                定义在外的有inline是内联,没有inline不是内联。

                内联函数与多文件

                内联函数可以有多个定义,多个定义必须完全一致,所以通常在内联函数写在头文件。

              数据成员

                相对特殊

                引用、const成员,初始化列表初始化;静态成员,类外初始化,无this指针;静态常量成员,类内和类外初始化;指针成员,注意拷贝构造产生的问题。

                this

                定义:this是一个指针,指向当前调用成员函数的对象。

                作用:this指针使得成员函数能够访问和操作对象的成员数据和其他成员函数。对象创建时才有的。

                类型:对应对象的类的指针类型。

                this指针不是成员,this的作用域在类内,系统默认传递给函数的隐含参数。

        继承

        继承实例

                继承的作用

                代码复用,避免写重复的代码。

                继承的格式

                people为基类或者称父类,xiaoming为子类或称派生类。

class xiaoming : public people
{
    ...... 
}
                继承对象的声明以及成员调用

                普通对象:调用构造函数或者缺省构造,调用成员形式:类名.成员;

                指针对象:new分配内存,调用形式:类名->成员;

                基类也可以自己创建对象;

                继承的限定词

                private:父类中的public和protected在子类为private,降低访问权限;

                protected:父类的public成员在子类为protected,降低权限;

                public:父类的权限是什么样的在子类就是什么样的;

访问publicprotectedprivate
同一个类yesyesyes
(public继承)派生类yesyesno
外部类yesnono
                成员的继承
                函数成员

                构造函数

                父类的构造函数可以通过子类构造函数的初始化列表调用。子类和父类的构造函数执行顺序是先父类再子类;

class base
{
    public:
        base(int x): val(x) {}
        base(double): val(static_cast<int>x) {}
    private:
        int val;
}
class Derived : public base
{
    using base::base;
    Derived() : base(0), str('c') {}
}

int main()
{
    Derived d(1);
    Derived d(3.14);
    Derived d();
    return 0;
}

                析构函数

                按顺序调用,辈分由小到大调用,与构造的顺序相反。

                覆盖

                父类和子类出现同名成员时,会覆盖;类内:子类覆盖父类,可以通过作用域区分;类外:类名作用域区分;父类子类函数相同名没有重载关系

                友元

                友元函数不能被继承。

                组合与继承的优缺点
继承组合
优点子类可以重写父类的方法,实现对父类扩展

1.当前对象只能通过所包含的对象调用方法,不可见被包含对象的内部实现

2.当前对象与包含对象是低耦合关系,如果修改包含对象的代码,不需要修改对象的代码

3.当前对象可以在运行是动态绑定所包含的对象,可以通过set方法给所包含对象赋值

缺点

1.子类可见父类的内部实现

2.子类从父类继承的方法在编译是确定下来了,所以无法在运行期间改变从父类继承的方法的行为

3.高耦合,不符合面向对象的思想

1.容易产生过多的对象

2.为了能组合对象,必须仔细对接口定义

        虚继承

                用于解决多重继承的菱形问题,虚基类只会保留一个实例,解决多继承中访问不明确的问题,结构复杂,内存开销比较大;

#include <iostream>

class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    virtual ~A() { std::cout << "A destructor" << std::endl; }
    int value;
};

class B : virtual public A {
public:
    B() { std::cout << "B constructor" << std::endl; }
    virtual ~B() { std::cout << "B destructor" << std::endl; }
};

class C : virtual public A {
public:
    C() { std::cout << "C constructor" << std::endl; }
    virtual ~C() { std::cout << "C destructor" << std::endl; }
};

class D : public B, public C {
public:
    D() { std::cout << "D constructor" << std::endl; }
    virtual ~D() { std::cout << "D destructor" << std::endl; }
};

int main() {
    D d;
    d.value = 42;
    std::cout << "d.value = " << d.value << std::endl;
    return 0;
}
                菱形问题

                D继承B、C,重复了基类的实例;同时D通过B、C两个路径访问A,会存在二义性;

        A
       / \
      B   C
       \ /
        D

        多态与虚函数 

        多态指对象在不同的情况下表现出不同的行为,虚函数是实现这个思想的语法基础。

        多态分为两种,动态多态和静态多态;动态多态:主要由虚函数实现,运行时决定调用哪个函数;静态多态:主要由函数重载和运算符重载实现,由编译器决定调用哪个。

        多态是一种泛型编程思想,使程序在不同类型的对象以一种统一的方式进行操作。

        重载运算符

        赋值运算符=、下表运算符[]、函数调用运算符()、->只能通过成员函数重载

        <<和>>只通过全局函数配合友元函数重载

        不要重载&&和||,无法实现短路特性

        对于内置数据类型,编译器知道如何做运算

        前置后置递增运算符:前置先++,然后返回自身;后置先保存原有值,内部++,最后返回临时数据

        重载后置++,要有占位参数,区分前后置

        重载赋值运算符:系统默认提供赋值运算符只是简单拷贝,导致类中指向堆区的指针,出现浅拷贝的问题,所以要重载=,若想要链式编程,return *this,返回引用;

        1.只能重载已有的运算符,无权发明新的运算符;对于一个重载运算符,其优先级和结合律与内置类型一致才可以;不能改变操作符操作数个数;

        2.两种重载方式:成员运算符和非成员运算符(友元函数),成员运算符比非成员运算符少一个参数;下表运算符、箭头运算符必须是成员运算符;

        3.引入运算符重载,是为了实现多态;

        4.当重载的运算符是成员函数时,this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个,至少含有一个类类型的参数;

        5.从参数的个数推断到底定义的哪种运算符,当运算符既是一元运算符又是二元运算符(+、-、*、&)

        6.下标运算符必须是成员函数,下标运算符通常以所访问元素的引用作为返回值,同时最好定义下标运算符的常量版本和非常量版本;

        7.箭头运算符必须是类的成员,解引用通常也是类的成员;重载箭头运算符必须返回类的指针。

                父类指向子类空间

                如下代码所示,a作为基类指针指向B的空间,a->show()调用的是B的函数,是通过虚表调用的;但是,a不可以访问B的showb()函数,showB()在A这是未知的。

                一个父类指针可以有多种执行状态,成为多态。

class A
{
    public:
        virtual void show {cout<<"A"<<endl;}
}

class B : public A
{
    public:
        virtual void show {cout<<"B"<<endl;}
        void showB {cout<<"show B"<<endl;}
}

int main()
{
    A* a = new B();
    a->show(); 
    return 0
}
                特点
                1.重写

                子类与父类的函数名字参数相同,父类函数声明virtual,子类重写父类的函数,可以加virtual也可以不加。

                返回值类型相同为重写,返回值类型不同为协变

                2.不是内敛函数
                3.构造函数不能是虚函数

                虚函数依赖虚函数表的初始化,在对象的构造过程中,虚函数表未完全设置。调用构造函数还不能确定对象的真实类型;构造函数的作用是初始化,在类的生命周期中只执行一次,不是对象的动态行为,没有必要成为虚函数。

                在构造函数中可以调用虚函数,此时的虚表指针指向该类的虚表,调用的函数是正在构造的类中的虚函数,而不是子类的虚函数。

                析构函数可以设置为虚函数:一般情况基类的析构函数要定义为虚函数,只有在基类析构函数为虚函数,基类指针调用delete操作符销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上调用虚函数),防止内存泄漏,析构函数可以是纯虚函数。

                虚表
                虚表的覆盖原理

                根据父类指针找到父类的函数,判断父类的函数是不是虚函数,若是,则进入虚表执行重写的;详见父类指向子类空间的代码。

                虚表的地址:对象空间开头的四字节内容是虚表地址。

                实现多态的过程

                (1)编译器发现基类中有虚函数,会自动为每个有虚函数的类生成一份虚表,表是一维数组,需表里保存了虚函数的入口地址。

                (2)编译器在每个对象的前四个字节保存一个虚表指针,*vptr,指向对象所属类的虚表。在构造时,根据对象的类型取初始化虚指针vptr,让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。

                (3)在派生类定义对象时,程序会自动调用构造函数,在构造函数创建虚表并对虚表初始化。在构造子类的对象时,会先调用父类的构造函数,此时编译器只看到父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。

                (4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向基类虚表;当派生类对基类的虚函数重写时,则派生类的虚表指针指向自身的虚表。

                虚表函数位于只读数据段(.rodata)即常量区,虚函数位域代码段(.text)即代码区。

                纯虚函数

                有纯虚函数的就是抽象类,抽象类不能被实例化,只能被继承。

                没有函数,继承这个基类的子类必须实现函数,才能被实例化,不然也是抽象类。 

                接口类:全是纯虚函数,可以有构造函数和成员。

virtual void fun() = 0;

模板

        函数模板

        意义:模板是泛型编程的一种重要思想。stl就是利用模板实现的一个具体实例。

template <typename T>
void fun(T t)
{
    cout<<t<<endl;
}
也可以多个参数
template <typename T, typename B>
void fun1(T t, B b)
{
    cout<<t<<b<<endl;
}
typename和class可以相互替换

         作用域:仅对下面挨着的代码段有效。

        具体化

template <typename T>
void fun(T t)
{
    cout<<t<<endl;
}
template <>
void fun<int>(int t)
{
    cout<<t<<endl;
}

        调用顺序:原版函数->具体化->模板

        实例化过程

        1.模板解析:编译器分析模板函数定义,根据提供的类型参数生成代码。

        2.类型替换:模板参数T被实际类型替换,生成适用类型的代码。

        3.函数生成:编译器实现函数,生成可执行二进制文件。

        类模板

template <typename T>
class A
{
    public:
        T val;
        void show(T t);
        void getval(T t) {val = t;}
        void A(const A<T> &a) : val(a.val) {}
        //A<T>与A<U>,T代表同类型,U代表不同类型
}
template <typename T>
void A<T>::show()
{
    val = t;
    cout<<val<<endl;
}

template <typename T>
class B : public A<T>
{
T
}

int main()
{
    A* a1 = new A<int>;
    A<int> a2;
}

        多态模板

A<short, int>* a = new B;
//子类没模板
A<short, int>* a1 = new B<short, int>;
//子类有模板,类型要对上

常用STL

        vector

template<typename T>
    class vector
	{
	public:		
        typedef T valueType;
		typedef T* iterator;
    private:
		iterator _start;
		iterator _finish;
		iterator _endOfStorage;
    }

        _start:容器起始的地址

        _finish:容器最后一个元素末尾的地址

        _endOfStorage:容器所占内存结束的地址

        内存管理

        :若容器空间不足,则申请更大的内存空间,移动数据到新的空间,再释放原先的空间,gcc扩大到两倍,vs扩大到1.5倍(扩大经历释放,重写分配内存,元素移动等操作)。

        push_back():需要创建临时对象,再移动到末尾。

        emplace_back():不需要额外的移动和拷贝操作,更高效简洁。

        memcpy浅拷贝问题:memcpy进行的是位操作,如果只是对普通数据类型int、char等赋值没问题,但如果对指针赋值就会出现浅拷贝问题。

        :只删元素,所占内存大小不变。

        erase()迭代器失效问题:当容器erase后,迭代器所指向的内容有可能发生改变,同时可能会产生野指针,当数据向前挪动,_finish可能会错过迭代器,从而使迭代器继续向后移动。

        reserve():改变的是容器的容量,如果new_cap小于容器当前容量,则容器容量不变。

        resize():改变元素的个数,如果new_size比原先的小,则删除多余元素,但不缩容(防止频繁地增删向操作系统申请、释放内存造成抖动)。

        string

class string
{
    private:
	    char* _ptr;					
	    size_t _size;				
	    size_t _capacity;				
}

        _ptr:实际存储字符内容的空间

        _size:存储的字符大小

        _capacity:容器目前最大的容量

        内存管理

        使用std::allocator分配和释放内存;同时使用小字符串优化(SSO),将较小字符串直接存在string对象内部的固定大小缓冲区中;增长策略,当容器需要扩容时,将以指数倍形式扩容。

        map

        实现为自平衡二叉搜索树(如红黑树),查找速度为O(logn)取决于树的高度,map拥有的所有元素都是pair,拥有键值对,且不允许相同的键。

        红黑树

        1.它是二叉排序树

        2.树中的所有节点非红即黑

        3.根节点必为黑

        4.红节点的子节点必为黑(黑节点的子节点可以为黑)

        5.根到null的路径上的黑节点树相同

        6.查找速度为O(logn)

        unordered_map与map的区别

        map支持自动排序,底层机制是红黑树,查询和维护的时间复杂度为O(logn),但空间占用大,需要保存父节点、子节点和颜色的信息。

        unordered_map底层机制是哈希表,通过哈希函数计算元素位置,查询时间复杂度为O(log1),维护时间与bucket桶所维护的list长度有关,但是建立hash表耗时大。

        map适用于频繁增删的有序数据应用场景,unordered_map适用于高效查询的应用场景。

        stack和queue

        stack是一种先进后出(FILO)的数据结构,提供了一种高效的数据结构,适合快速插入与删除的场景,默认由deque实现;queue是一种先进先出(FIFO)的数据结构,queue可以使用list作为底层容器,不具有遍历功能,没有迭代器。

        deque

        deque是一种双向开口的连续线性空间,vector是单向开口连续线性空间;deque和vector最大差异是deque运行在常数时间内对头端进行元素操作和deque没有容量的概念,它是以动态地以分段连续空间组合而成,可以随时增加一段新的空间并链接起来。

        deque虽然也提供随机访问的迭代器,但其迭代器并不是普通指针。如果对deque排序,可以先将deque中的元素复制到vector,利用sort对vector排序,再赋值给deque。

        deque由一段一段的定量连续空间组成,一旦需要增加新的空间,只要配置一段定量连续空间拼接在头部或尾部即可。

        内存管理

        deque内部有一个指针指向map,map是一小块连续空间,其中的每个元素称为节点,每个node都是一个指针,指向另一段较大的连续空间,称为缓冲区,这里就是deque中实际存放数据的区域,默认大小512bytes。

        deque迭代器的++和--其主要工作在缓冲区边界,从当前缓冲区跳到另外一个缓冲区,当然deque内容在插入元素时,如果map中node数量完全使用完,且node指向的缓冲区也没有多余的空间,这时会配置新的map(2倍于当前+2的数量)来容纳更多的node。在deque删除元素时,也提供了元素的析构和空闲缓冲区空间的释放等机制。

        list

        底层实现是双向链表。

        forward_list

        单向链表容器,适用于高效删除插入,但不需要双向遍历的场景。

C++新特性

        关键词

        auto

        编译器根据情况确定auto变量的真正类型,auto作为函数返回值时,只能用于定义函数,不能用于申明函数。

        nullptr与NULL的区别

        nullptr明确表示空指针;NULL通常是一个宏定义,常被定义成0或(void*)0,用来表示无效指针。

        nullptr作为空指针常量,其类型是std::nullptr_t,这使它的类型安全,不能被隐式转换为其他非指针类型,避免类型错误;NULL由于可以被定义为0,会存在引发类型不明确的问题。

        atomic

        C++11封装的原子数据类型,从功能上看,原子数据类型不会发生数据竞争,不必用户对其添加互斥资源锁类型。

        智能指针与内存管理

        智能指针:是一个类,用于自动管理动态分配内存的生命周期,避免内存泄漏和指针悬挂。

        shared_ptr(共享指针)

        实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数减1,当计数器为0时自动释放动态分配内存的资源。

        1.智能指针将一个计数器与类指向的对象关联,引用计数器跟踪公有多少个类对象共享同一指针。

        2.每次创建类的新对象时,初始化指针并将引用计数置为1。

        3.当对象作为另一个对象的副本而创建时,拷贝构造函数拷贝指针增加与之相应的引用计数。

        4.对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加有操作数所指对象的引用计数。

        5.调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

        unique_ptr(独享指针)

        实现原理:采用的是多线美国所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移(赋值,使用move()函数)一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。所以unique_ptr不支持普通的拷贝和赋值操作,不能在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁,不会多个对象指向同一个资源);如果你拷贝一个unique_ptr,那么拷贝结束后,两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。unique_ptr作为STL容器的元素时,不能作为在类内的成员变量。

        shared_ptr和unique_ptr的区别

        1.shared_ptr是共享所有权,支持移动、赋值和拷贝;unique_ptr是独占所有权,不支持赋值和复制,支持移动。

        2.unique_ptr默认情况下和裸指针是一样的,内存没有额外消耗,性能最优;shared_ptr除了管理一个裸指针以外,还要委会一个引用计数,所以内存占用是裸指针的两倍(x64为16字节,x86为8字节);

        shared_ptr的实现

        shared_ptr的实现是利用内部的引用计数来实现管理,每当复制一个shared_ptr,计数器加1 ;shared_ptr生命周期结束时,计数器减1。当引用计数为0时,用delete释放内存。

        shared_ptr应有三个成员:一个裸指针,指向要管理的对象;一个强引用计数,记录shared_ptr指向裸指针的数量;一个弱引用计数,记录weak_ptr指向裸指针的数量。

        weak_ptr(弱指针)

        实现原理:弱指针。引用计数存在一个问题,当shared_ptr互相引用形成环,两个指针指向的内存都无法释放,需要weak_ptr打破环形引用。weak_ptr是为了配合shared_ptr而引用的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定有效,在使用前用lock()和expired()函数检查weak_ptr是否为空指针。

        lock():用于获取一个shared_ptr实例。若shared_ptr对象被销毁,则返回空的shared_ptr。可以将weak_ptr转换成shared_ptr。

        expired():若被观测对象shared_ptr被销毁,则返回true。

        为什么使用make_shared、make_unique

        分配的的时候只分配一次,效率更高;使用智能指针的初始化函数需要分配两次。

        智能指针的循环引用

        多个智能指针shared_ptr时,出现了指针之间相互指向,从而形成环的情况,有点类似于死锁的情况。p1和p2互相引用,假设释放p1,调用析构时因为p1引用p2,所以先调用p2的析构,但p2引用p1,要调用p1的析构,就形成死循环,最终造成内存泄漏。

        应尽量避免出现智能指针之间互相指向的情况,如果不可避免,可以使用weak_ptr,它不增加引用计数,只要出了作用域就会自动析构。

        手写实现智能指针类需要实现哪些函数

        一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,救释放该对象。

        除了指针对象外,还需要一个引用计数的指针设定对象的值,并将引用计数设为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。

        通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1。

        一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数。

        shared_ptr是线程安全吗

        不是,引用计数是线程的安全的,shared_ptr资源所有权的操作不是原子的。

        其他

        std::fuction

        是一个函数包装器,可以复制、存储和调用任何可以调用的目标(函数指针、lamda表达式、成员函数指针和其他可调用对象)

void fun(int x)
{
    cout<<x<<endl;
}

int main()
{
    std::function<void(int)> f1 = fun;
    f1(10);
    return 0;
}

        std::bind

        函数适配器,创建一个函数新对象,该对象绑定到特定的参数。

void fun(int x, int y)
{
    cout<<x+y<<endl;
}

int main()
{
    auto bindFunction = std::bind(fun, 5, std::placeholders::_1);//把5绑定到x
    bindFunction(10); // y=10
    return 0;
}

        lamda表达式

        []用于捕获外部变量

int main()
{
    int a = 5;
    int b = 10;

    auto show1[]()
    {
        cout<<"show1"<<endl;
    }

    auto show2[a]()
    {
        cout<<a<<endl;
    }

    auto show3[&b]() //引用b
    {
        cout<<b<<endl;
    }

    auto show4[](int x, int y)
    {
        return x+y;
    }

    show1(); //输出结果:show1
    show2(); //输出结果:5
    show3(); //输出结果:10
    b = 14;
    show3(); //输出结果:14
    int res = show4(a,b); // res = 19
}

内存管理

        内存分布

                

        代码区(.text)

        存放程序的机器码和只读数据,可执行指令就是从这得到的。该段被标记为只读,任何对该区的写操作会导致段错误(Segmentation Fault)。

        常量区(.rodata)

        存放常量字符串。

        全局区/静态区(.data和.bss)

        分初始化数据段(.data)和未初始化数据段(.bass)

        (.data)用于存放已初始化的全局变量和静态变量。

        (.bss)用于存放未初始化的全局变量和静态变量。

        .data和.bss分开的原因

        关系

        .text部分是编译后程序的主题,也就是程序的机器指令。

        .data和.bss保存了程序的全局变量,.data保存有初始化的全局变量和静态变量,.bss保存未初始化的全局变量和静态变量。

        .text和.data在可执行文件中(在嵌入式中一般是固化在镜像文件中),由系统从可执行文件中加载。

        .bss不在可执行文件中,有系统初始化。

        结论

        1..data

        数据部分包含初始化的数据项的数据定义。初始化数据是在程序在开始运行之前具有值的数据。这些值是可执行文件的一部分。当将可执行文件加载到内存中以供执行时,它会加载到内存中。

        定义的初始化数据项越多,可执行文件越大,并且在运行它时将其从磁盘加载到内存所需的时间也越长。

        2..bss

        在程序开始运行之前,并非所有数据项都需要具有值。.bss部分定义了数据缓冲区,为缓冲区留出一定数量的字节,并为缓冲区指定了名称。所以可执行文件中它不会占用实际的存储空间。

        堆区(heap)

        用来存储程序运行时分配的变量。堆的大小不固定,可动态扩张和缩减,有malloc和new这类内存分配函数实现。没有被程序员释放的内存会在进程结束后操作系统自动回收。

        文件映射区

        将文件内容映射到进程的虚拟内存空间。通过这种技术,进程可以像访问内存一样访问文件内容,而无需进行文件的I/O操作。文件映射区用于高效读写大文件或在不同进程间共享数据。

        优点

        1.高效性:通过内存映射文件,避免传统的I/O操作,从而提高文件访问的效率。映射文件时,操作系统会将文件内容直接映射到内存中。

        2.简化编程:内存映射文件使程序像操作内存一样操作文件,减少显示的读写操作。

        3.共享内存:不同进程可以映射同一个文件到各自的虚拟地址空间,实现进程间的共享数据。

        栈区(stack)

        用来存储函数调用时的临时信息的结构,如函数调用所传递的函数、函数的返回地址、函数的局部变量等。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。

        堆和栈的区别

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

        函数调用过程栈的变化,返回值和参数变量哪个先入栈?

        1.调用者函数把被调函数所需的参数按照从右到左依次把参数压入栈中。

        2.调用者函数使用call命令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(压栈指令隐藏在call中)。

        3.在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后在保存调用者函数的栈顶地址(move ebp, esp)。

        4.在被调函数中,从ebp的位置开始开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小。

        printf函数的实现原理

        C/C++对函数的参数扫描是从后向前的。printf函数第一个被找到的参数是字符串,根据字符串的控制参数的个数判断参数个数和数据类型,可以算出数据需要的堆栈指针的偏移量。

        类对象存储空间

        非静态成员的数据类型大小之和。编译器加入的额外成员变量(如指向虚表的虚表指针),为了边缘对齐优化加入的padding。

        空类(无非静态数据成员)的对象的size为1,当作为基类时,size为0。

        什么是内存池,如何实现

        内存池(Memory Pool)是一种内存分配方式。new和malloc等内存申请方式,缺点在于:由于所申请的内存块的大小不一定,当频繁使用会造成大量内存碎片进而降低性能。内存池则是在真正使用内存前,先申请分配一定数量的、大小相等的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样尽量避免内存碎片,使得内存分配效率得到提升。

        STL的内存池实现机制

        allocate包装malloc,deallocate包装free

        一般是一次20*2个的申请,先用一半,留一半。

        1.首先客户端会调用malloc()配置一定数量的区块(固定大小的内存块,通常为8的倍数),假设40个32bytes的区块,其中20个区块(一半)给程序实际使用,1个区块交出,另外19个处于维护状态。剩余20个留给内存池,此时一共有(20*32bytes)

        2.客户端之后有内存需求,想申请(2064bytes)的空间,这时内存池只有(2032),就先将(10*64bytes)个区块返回,1个区块交出,另外9个处于维护状态,此时内存池空空如也

        3.接下来如果客户端还有内存需求,就必须再调用malloc()配置空间,此时新申请的区块数量会增加一个随着配置次数越来越大的附加量,同样一半提供程序使用,另一半留给内存池。申请内存的时候永远是先看内存池是否有无剩余空间,有就用上,然后挂在0-15号某条链表上,不然就重新申请。

        4.如果整个堆空间都不够用了,就会在原先已经分配的区块中寻找能满足当前需求的区块数量,能满足就返回,不能就向客户端报bad_alloc异常。

        allocator和deallocator的内部实现,目前所有编译器都是直接调用operator new和operator delete,和直接使用new的效果是一样的,是将上没有任何特殊作用。

        在GC2.9之前

        new和operator new的区别:new是个运算符,编辑器会调用operator new(0)

        operator new()里面有调用malloc的操作,那同样的operator delete()里面有调用free的操作

        GC4.9及之后还有,但需要自己指定变回对malloc和free的包装形式。

        this指针问题

        this指针:this指针是类的指针,指向对象的首地址;this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this;this指针只有成员函数中才有定义,且存储位置会因编译器不同有不同的存储位置。

        this指针的用处

        一个对象的this指针并不是对象本身的部分,不会影响sizeof的结果。this的作用域在类的内部,当在类的非静态成员函数中访问类的静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。

        this指针的使用

        在类的非静态成员函数中返回类对象本身的时候,直接使用return *this;另一种情况是当形参与成员变量名相同时用于区分,如this->n=n;

        this指针什么时候创建的

        this在成员函数的开始执行前构造,在成员执行结束后清除。

        但是如果class和struct里没有函数,它们是没有构造函数的,只能当作C的struct使用。采用TYPE的方式定义,在栈里分配内存,this指针的值就是这块内存的地址。采用new创建,在堆里分配内存,new操作通过eax(累加寄存器)返回分配地址,然后设置给指针变量。之后调用构造函数,这是将这块内存块的地址传给ecx(寄数寄存器)。

        this指针存放在何处

        this指针会因编译器不同而放在不同位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它并不是和高级语言变量对应的。

        this指针是如何传递类中的函数的,this又是如何找到类实例后的函数

        大多数编译器通过ecx(寄数寄存器)寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。

        在call之前,编译器会把对应的对象地址放到eax(累加寄存器)中。this是通过函数参数的首参来传递。this指针在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那,不会跑。

        获得一个对象,才能通过对象使用this指针,如果知道一个对象this指针的位置,可以直接使用吗

        this指针只有成员函数中才有定义。因此,获得一个对象后,也不能通过对象使用this指针。无法知道一个对象的this指针的位置(只有成员函数里才有this指针的位置)。当然,在成员函数里,你可以知道this指针的位置的,也可以直接使用。

        内存泄漏的后果?如何检测?解决方法?

        (1)内存泄漏

        由于疏忽或错误造成程序未能释放掉不再使用的内存。

        (2)后果

        一次小的内存泄漏可能不被注意,但大量泄漏内存会出现征兆:性能下降到内存用完,导致一个程序失败。

        (3)如何排除

        使用BoundChecker软件,调试运行DEBUG版程序,运用以下技术:CRT(C run-time libraries)、运行时函数调用堆栈、内存泄漏时提示的内存分配序号,综合分析内存泄漏的原因,排除内存泄漏。

        (4)解决办法

        头文件<crtdbg.h>

        查找:在mian函数最后一行,加上一句_CtrDumpMemoryLeaks()。调试程序自然关闭程序让其退出,查看输出{453}normal block at 0x02432CA8,868 bytes long。{453}是内存泄漏位置,868比特没释放。

        定位代码位置:在main函数第一行加上_CtrSetBolckAlloc(453),在453这块内存设置中断。然后调试程序,程序断了,查看调用堆栈。

编译

        c++从代码到可执行程序经历了什么

        (1)预编译

        主要处理源代码文件中的以#开头的预编译指令,处理规则如下:

        1.删除所有的#define,展开所有的宏定义。

        2.处理所有的条件预编译指令,如"#if"、"#endif"、"#ifdef"、"#elif"和"#else"。

        3.处理"#include"预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。

        4.删除所有的注释。

        5.保留所有的#pargma编译指令,编译器需要用到他们,如:#pargma once是为防止有文件被重复引用。

        6.添加行号和文件标识,便于编译时编译器产生调试用的型号信息,和编译时产生编译错误或警告时能够显示行号。

        (2)编译

        把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。

        1.词法分析:利用类似于"有限状态机",将源代码程序输入到扫描机中,将其中的字符序列分割成一些的记号。

        2.语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。从语法分析器输出的语法树是一种以表达为节点的树。

        3.语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义--在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。

        4.优化:源代码级别的一个优化过程。

        5.目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列--汇编语言表示。

        6.目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

        (3)汇编

        将汇编代码转换成机器可以执行的指令(机器码文件)。汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表--翻译过来,汇编过程有汇编器as完成。经编译后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

        (4)连接

        将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接动态链接

        静态链接

        函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来的创建最终的可执行文件。

        空间浪费:因为每个可执行程序中对所有需要的目标文件都有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本。

        更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

        运行速度快::但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

        动态链接

        动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样吧所有程序模块都链接成一个单独的可执行文件。

        共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分副本,而是这个多个程序在执行时共享同一个副本;

        更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并链接起来,程序就完成了升级的目标。

        性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能有一定损耗。

        静态编译和动态编译

        1.静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

        2.动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了可执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕很简单程序,只要用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

        hello.c程序的编译过程

gcc hello.c -o hello

        过程大致如下:

        预处理阶段:处理以#开头的预处理命令;

        编译阶段:翻译成汇编文件;

        汇编阶段:将汇编文件翻译成可重定位目标文件。

        链接阶段:将可重定位目标文件和printf.o等单独编译好的目标文件进行合并,得到最终可执行目标文件。

        静态链接

        静态链接器以一组可重定位目标文件为输入,生成一个完成链接的可执行目标文件作为输出。链接器主要完成以下两个任务:

        符号解析:每个符号对应一个函数、一个全局变量或静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。

        重定位:链接器通过把每个符号定义与一个内存位置相关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

        目标文件

        可执行文件:可以直接在内存中执行;

        可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;

        共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接。

        动态链接

        静态库有以下两种问题:

        1.当静态库更新时那么整个程序都要重新进行链接;

        2.对于printf这种标准函数库,如果每个程序都要有代码,这会极大浪费资源;

        共享库是为了解决静态库的这两个问题设计的,在Linux系统中通常用.so后缀来表示,Windows系统上它们被称为dll。它们具有以下特点:

        1.在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用到它的可执行文件中;

        2.在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

        源代码->预处理->编译->优化->汇编->链接->可执行文件

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐