C++秋招必知必会(21-40)

参考引用

21. define 宏定义和 const 的区别

  • define 是在预编译阶段起作用,属于文本插入替换,而 const 是在编译、运行的时候起作用
  • define 只做替换,不做类型检查和计算,容易出错,const 常量有数据类型,编译器可以对其进行类型安全检查
  • define 只是将宏名称进行替换,在内存中会产生多份相同的备份,const 在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表

22. C++ 中 const 和 static 的作用

  • static 不考虑类的情况

    • 隐藏。所有不加 static 的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    • 默认初始化为 0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
  • static 考虑类的情况

    • static 成员变量
      • 所有对象共享同一份数据,在编译阶段分配内存,类内声明类外初始化
      • 只与类关联,不与类的对象关联
      • 定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为 static
      • 可以被非 static 成员函数任意访问
    • static 成员函数
      • 所有对象共享同一个函数,静态成员函数只能访问静态成员变量
      • 静态成员函数属于类,而非特定对象实例,所以无法使用 this 指针,无法访问类对象的非 static 成员变量和非 static 成员函数
      • 可以被非 static 成员函数任意访问
      • 不能被声明为 const 和 virtual

      1、static 成员函数是类所有对象的共享成员,不属于任何类对象或类实例,所以即使给此函数加上 virutal 也是没有任何意义的,virtual 虚函数依靠 vptr 和 vtable 来处理,vptr 是一个指针,在类的构造函数中创建生成,并且只能用 this 指针来访问它,因为它是类的一个成员,而 static 成员函数不具有 this 指针,虚函数的调用关系:this -> vptr -> vtable ->virtual function
      2、当声明一个非静态成员函数为 const 时,对 this 指针会有影响,而 static 成员函数没有 this 指针,所以使用 const 来修饰 static 成员函数没有任何意义

  • const 不考虑类的情况

    • const 常量在定义时必须初始化,之后无法更改
    • const 形参可以接收 const 和非 const 类型的实参
    • const 修饰变量与 static 有一样的隐藏作用:只能在该文件中使用,其他文件不可使用
  • const 考虑类的情况

    • const 成员变量
      • 不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数
      • 不同类对其 const 数据成员的值可以不同,所以不能在类中声明时初始化
    • const 成员函数
      • const 对象不可以调用非 const 成员函数,非 const 对象都可以调用
      • 不可以改变非 mutable(用该关键字声明的变量可以在 const 成员函数中被修改)数据的值

23. 数组名和指针(这里为指向数组首元素的指针)区别

  • 二者均可通过增减偏移量来访问数组中的元素
  • 数组名不是真正意义上的指针,数组名没有自增、自减等操作
  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但 sizeof 运算符不能再得到原数组的大小了

数组和指针的区别

  • 存储方式。数组在内存中连续存放,开辟一块连续的内存空间,数组是根据数组下标进行访问的;指针相当于一个变量,可以指向任意类型的数据,指针的类型说明了它所指向地址空间的内存,指针的存储空间不能确定
  • 访问方式。数组的元素可以通过下标来访问,下标从 0 开始,最大下标为数组大小减 1;指针可以通过加减运算来访问它所指向的数组元素,但需要注意指针的类型,以及要访问的数组元素的类型
  • 动态分配。指针可以通过malloc()和free()来分配空间和释放空间,这是动态分配空间;数组是隐式分配和删除,这是静态分配空间
  • 安全性。指针使用不当会造成内存泄漏,数组使用不当会造成数组越界;指针名是变量,数组名是指针常量,所以指针 p 可以进行 p++,而数组名不可以用于 a++

24. override 和 final 关键字

  • override

    • 当在父类中使用了虚函数,可能需要在某个子类中对这个虚函数进行重写
    • override 指定子类的这个虚函数是重写父类的,如果函数名不小心打错,编译器不会编译通过
    class A {
        virtual void foo();
    }
    class B : public A {
        void foo();            // OK,对虚函数 foo() 进行重写
        virtual void foo();    // OK,对虚函数 foo() 进行重写
        void foo() override;   // OK,对虚函数 foo() 进行重写
    }
    
  • final

    • 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加 final 关键字,添加 final 关键字后被继承或重写,编译器会报错
    class Base {
        virtual void foo();
    };
     
    class A : public Base {
        void foo() final; // foo 被 override 并且是最后一个 override,在其子类中不可以重写
    };
    
    class B final : A {      // 指明 B 是不可以被继承的
        void foo() override; // Error: 在 A 中已经被 final 了
    };
     
    class C : B    // Error: B is final
    {};
    

25. 拷贝初始化和直接初始化

  • 当用于类类型对象时
    • 直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数
    • 拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象
    string str1("I am a string");   // 语句1 直接初始化
    string str2(str1);              // 语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
    string str3 = "I am a string";  // 语句3 拷贝初始化,先为字符串创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
    string str4 = str1;             // 语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
    

    为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样拷贝初始化就完全等价于直接初始化了,但是需要辨别两种情况

    • 拷贝构造函数为 private 时:语句 3 和语句 4 在编译时会报错
    • 使用 explicit 修饰构造函数时:如果构造函数存在隐式转换,编译时会报错

26. extern 关键字的用法

  • extern 用于变量
    • 当需要在多个文件中共享同一个全局变量时,可以使用 extern 关键字
    // 文件1: main.cpp 
    extern int shared_var;  // 声明一个外部整型变量
    
    int main() { 
        shared_var = 10;    // 使用外部变量    
        return 0;
    }
    
    // 文件2: shared.cpp
    int shared_var = 0;     // 定义一个全局整型变量
    
  • extern 用于函数
    • extern 关键字也可以用于在一个文件中引用另一个文件中定义的函数
    // 文件1: main.cpp
    extern void print_message();  // 声明一个外部函数
    int main() { 
        print_message();          // 调用外部函数 
        return 0;
    }
    
    // 文件2: print.cpp
    #include <iostream>
    void print_message() {        // 定义一个函数    
        std::cout << "Hello, World!" << std::endl;
    }
    
  • extern “C” 的用法
    • 如果想在 C++ 代码中调用 C 代码,或者在 C 代码中调用 C++ 代码,就需要用到 extern “C”
    // 文件1: main.cpp (C++ 代码)
    extern "C" void print_message();  // 使用 extern "C" 声明一个外部函数
    int main() {    
        print_message();  // 调用外部函数    
        return 0;
    }
    
    // 文件2: print.c (C 代码)
    #include <stdio.h>
    
    void print_message() {  // 定义一个函数    
        printf("Hello, World!\n");
    }
    

27. 野指针和悬空指针

都是指向无效内存区域(“不安全不可控”)的指针,访问行为将会导致未定义行为

  • 野指针,指的是没有被初始化过的指针
    • 解决办法:1、定义指针变量及时初始化,或者先置空 NULL 可防止指针误用;2、在指针被释放之后,将其赋值为 NULL 或 nullptr;3、使用智能指针,智能指针可以自动管理动态内存,避免内存泄漏和野指针问题 ;4、避免指针操作过程中越界访问数组,可以使用STL容器代替数组,STL容器可以自动管理内存
    int* p;  // 未初始化
    std::cout<< *p << std::endl;  // 未初始化就被使用
    
  • 悬空指针,指针最初指向的内存已经被释放的指针
    • 解决办法:释放操作后立即置空
    • C++ 引入了智能指针,目的就是避免悬空指针的产生
    int* p = nullptr;
    int* p2 = new int;
    p = p2;
    delete p2;
    

28. 重载、重写/覆盖和隐藏的区别

  • 重载(overload)

    • 同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现:参数个数和类型均相同,仅仅依靠返回值不同来区分的函数的情况
    • 重载是不同函数之间的水平关系
    • 重载要求参数列表不同,返回值不要求
  • 重写/覆盖(override)

    • 重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且与基类的虚函数:有相同的参数个数、相同的参数类型、相同的返回值类型
    • 重写是父类和子类之间的垂直关系
    • 重写要求参数列表相同
  • 隐藏(hide)

    • 隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数

29. 构造函数和析构函数

  • 对象的初始化和清理工作是编译器强制要做的事情,因此如果程序员不提供构造和析构,编译器会主动提供,但编译器提供的构造函数和析构函数是空实现,构造函数和析构函数作用如下:

    • 构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用(只调用一次),无须手动调用
    • 析构函数:析构函数在对象销毁前由系统自动调用(只调用一次),执行一些清理工作

    构造函数可以有参数,因此可以发生重载,而析构函数没有参数,因此不可以发生重载

  • 构造函数的分类及调用

    • 按参数分为:有参构造和无参构造(默认构造函数)
    • 按类型分为:普通构造和拷贝构造
    // 1、构造函数分类
    class Person {
    public:
        // 1.1 无参(默认)构造函数
        Person() {
            cout << "无参构造函数!" << endl;
        }
        // 1.2 有参构造函数
        Person(int a) {
            age = a;
            cout << "有参构造函数!" << endl;
        }
        // 1.3 拷贝构造函数
        Person(const Person& p) {
            age = p.age;
            cout << "拷贝构造函数!" << endl;
        }
        // 析构函数,在释放内存之前调用
        ~Person() {
            cout << "析构函数!" << endl;
        }
    public:
        int age;
    };
    
    // 2、构造函数的调用
    // 2.1 调用无参构造函数
    void test01() {
    	Person p;  // 调用无参构造函数
    }
    
    // 2.2 调用有参的构造函数
    void test02() {
        // 2.2.1 括号法,最常用
        Person p1(10);
        // 注意 1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
        //Person p2();
        
        // 2.2.2 显式法
        Person p2 = Person(10); 
        Person p3 = Person(p2);
        //Person(10)单独写就是匿名对象  当前行结束之后,马上析构
        
        // 2.2.3 隐式转换法
        Person p4 = 10; // Person p4 = Person(10); 
        Person p5 = p4; // Person p5 = Person(p4); 
        
        // 注意 2:不能利用拷贝构造函数初始化匿名对象,编译器认为是对象声明
        // Person p5(p4);
    }
    
    int main() {
        test01();
        //test02();
    
        system("pause");
        return 0;
    }
    
  • 拷贝构造函数调用时机

    // 1. 使用一个已经创建完毕的对象来初始化一个新对象
    void test01() {
        Person man(100);       // p 对象已经创建完毕
        Person newman(man);    // 调用拷贝构造函数
        Person newman2 = man;  // 拷贝构造
        
        //Person newman3;
        //newman3 = man;  // 不是调用拷贝构造函数,赋值操作
    }
    
    // 2. 值传递的方式给函数参数传值
    // 相当于 Person p1 = p;
    void doWork(Person p1) {}
    void test02() {
        Person p; //无参构造函数
        doWork(p);
    }
    
    // 3. 以值方式返回局部对象
    Person doWork2() {
        Person p1;
        cout << (int *)&p1 << endl;
        return p1;
    }
    void test03() {
        Person p = doWork2();
        cout << (int *)&p << endl;
    }
    
    int main() {
        //test01();
        //test02();
        test03();
        
        system("pause");
        return 0;
    }
    
  • 构造函数调用规则

    • 默认情况下,C++ 编译器至少给一个空类添加 6 个默认成员函数
      • 默认构造函数(无参,函数体为空)
      • 默认析构函数(无参,函数体为空)
      • 默认拷贝构造函数(对属性进行值拷贝)
      • 赋值操作符重载
      • 取地址操作符重载
      • const 修饰的取地址操作符重载
    • 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造
    • 如果用户定义拷贝构造函数,C++ 不再提供其他构造函数
  • 类成员初始化

    • 类成员初始化方式:赋值初始化、成员列表初始化
    • C++ 的赋值操作会产生临时对象,临时对象的出现会降低程序的效率,因此成员列表初始化方式更快,如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作
    class Person {
    public:
        // 1、赋值初始化
        //Person(int a, int b, int c) {
        //	m_A = a;
        //	m_B = b;
        //	m_C = c;
        //}
        
        // 2、成员列表初始化
        Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
    private:
        int m_A;
        int m_B;
        int m_C;
    };
    
    • 必须使用成员列表初始化的四种情况
      • 当初始化一个引用成员时
      • 当初始化一个常量成员时
      • 当调用一个基类的构造函数,而它拥有一组参数时
      • 当调用一个成员类的构造函数,而它拥有一组参数时
  • (派生类)构造函数的执行顺序

    • 1、虚基类的构造函数
    • 2、基类的构造函数
    • 3、成员类对象的构造函数
    • 4、派生类自己的构造函数

    析构函数顺序正好与上面顺序相反

  • 构造函数、拷贝构造函数和赋值操作符的区别

    • 构造函数
      • 对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数
    • 拷贝构造函数
      • 对象不存在,但是使用别的已经存在的对象来进行初始化
    • 赋值运算符
      • 对象存在,用别的对象给它赋值,属于重载 “=” 号运算符的范畴,“=” 号两侧的对象都是已存在的

    拷贝构造函数是函数,赋值运算符是运算符重载,拷贝构造函数会生成新的类对象,赋值运算符不能

30. 浅拷贝和深拷贝的区别

  • 浅拷贝
    • 浅拷贝只是简单的赋值拷贝操作,只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝指针的资源就会出现错误
  • 深拷贝
    • 深拷贝不仅拷贝值,还在堆区重新申请空间用来存放新的值,即使原先的对象被析构掉释放内存了,也不会影响到深拷贝得到的值
    • 在实现拷贝赋值的时候,如果有指针变量的话是需要实现深拷贝的
    #include <iostream>
    
    using namespace std;
    
    class Person {
    public:
        // 无参(默认)构造函数
        Person() {
            cout << "non para construct!" << endl;
        }
        // 有参构造函数
        Person(int age, int height) { 
            cout << "have para construct!" << endl;
            m_age = age;
            m_height = new int(height);
        }
        // 拷贝构造函数  
        Person(const Person& p) {
            cout << "copy construct!" << endl;
            // 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
            m_age = p.m_age;
            m_height = new int(*p.m_height); 
        }
        // 析构函数
        ~Person() {
            cout << "~!" << endl;
            if (m_height != NULL) {
                delete m_height;
            }
        }
    public:
        int m_age;
        int* m_height;
    };
    
    int main() {
        Person p1(18, 180);
        Person p2(p1);
        
        cout << "p1 age: " << p1.m_age << " height: " << *p1.m_height << endl;
        cout << "p2 age: " << p2.m_age << " height: " << *p2.m_height << endl;
        
        system("pause");
        return 0;
    }
    
    have para construct!
    copy construct!
    p1 age: 18 height: 180
    p2 age: 18 height: 180
    ~!
    ~!
    

31. C++ 类的访问权限和继承权限

  • 访问权限
    在这里插入图片描述

  • 继承权限:protected 继承和 private 继承
    在这里插入图片描述

public继承

  • 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问

友元(friend)

  • 有些私有属性也想让类外一些特殊的函数或者类进行访问,就需要用到友元的技术
  • 友元的目的就是让一个函数或者类访问另一个类中私有成员

32. mutable 和 explicit 关键字

  • mutable
    • 成员函数后加 const 称为常函数,常函数内不可以修改成员属性
    • 但如果成员属性声明时加关键字 mutable,则在常函数中依然可以修改
  • explicit
    • 用来修饰类的构造函数,被修饰的构造函数的类不能发生隐式类型转换,只能以显式方式进行类型转换
    • explicit 关键字只能用于类内部的构造函数声明上

C++ 隐式转换

  • 所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为
  • 通过隐式转换,可以直接将一个子类的对象使用父类的类型进行返回
  • 隐式转换发生在从小 --> 大的转换中:char --> int、int --> long
  • 在构造函数声明的时候加上explicit关键字,能够禁止隐式转换

33. C++ 异常处理方法

  • 常见的异常

    • 数组下标越界
    • 除法计算时除数为 0
    • 动态分配空间时空间不足
  • 异常处理方式

    • 1. try、throw 和 catch 关键字:先执行 try 语句块,如果执行过程中没有异常发生,则不会进入任何 catch 语句块,如果发生异常,则使用 throw 进行异常抛出,再由 catch 进行捕获
    double m = 1, n = 0;
    try {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;    // 抛出 int 型异常
        else if (m == 0)
            throw - 1.0;  // 拋出 double 型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) {
        cout << "catch (double)" << d << endl;
    }
    
    • 2. C++ 标准异常类 exception

34. 形参与实参的区别

  • 函数定义里小括号内的参数称为形参,函数调用时传入的参数称为实参
  • 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量
  • 实参可以是常量、变量、表达式和函数等,无论实参是何种类型的量,在进行函数调用时都必须具有确定的值,以便把这些值传送给形参
  • 实参和形参在数量上、类型上和顺序上应严格一致,否则会发生 “类型不匹配” 的错误
  • 只能把实参的值传送给形参,而不能把形参的值反向传送给实参,因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化

35. 值传递、指针传递和引用传递

  • 值传递
    • 当将一个值传递给函数时,函数会创建该值的副本,并在函数内部使用这个副本,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间(传值)
    • 对于简单的数据类型,如整数、字符等,值传递是最合适的选择,当函数不需要修改原始值,只是使用该值时,值传递也是一个不错的选择
  • 指针传递
    • 将参数的内存地址传递给函数(传地址值)
    • 当函数需要修改原始值时,指针传递是一个不错的选择,对于需要传递大型对象的情况,指针传递可以提高性能
  • 引用传递
    • 将参数的引用传递给函数,函数可以通过引用直接访问和修改原始值,而无需创建副本(传地址)
    • 当函数需要修改原始值时,引用传递是最常用的方式,对于大型对象的传递,引用传递可以提高性能
    • 指针传递和引用传递比值传递效率高,推荐使用引用传递
    • 不能返回局部变量的引用,因为函数返回后局部变量就会被销毁
    // 1. 值传递
    void mySwap01(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
    }
    
    // 2. 指针传递
    void mySwap02(int* a, int* b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    
    // 3. 引用传递
    void mySwap03(int& a, int& b) {
        int temp = a;
        a = b;
        b = temp;
    }
    
    int main() {
        int a = 10;
        int b = 20;
        
        mySwap01(a, b);
        cout << "a:" << a << " b:" << b << endl;
        mySwap02(&a, &b);
        cout << "a:" << a << " b:" << b << endl;
        mySwap03(a, b);
        cout << "a:" << a << " b:" << b << endl;
        
        system("pause");
        return 0;
    }
    
  • 指针传递和引用传递的使用时机(见 4)
    • 对于使用引用的值而不做修改的函数
      • 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
      • 如果数据对象是数组,则使用 const 指针(唯一的选择)
      • 如果数据对象是较大的结构,则使用 const 指针或者引用,已提高程序的效率
      • 如果数据对象是类对象,则使用 const 引用(传递类对象参数的标准方式是按照引用传递)
    • 对于修改函数中数据的函数
      • 如果数据是内置数据类型,则使用指针
      • 如果数据对象是结构,则使用引用或者指针
      • 如果数据是类对象,则使用引用

36. C++ 中 string 与 C 语言中的 char* 区别

  • string 继承自 basic_string,是对 char* 进行了封装,封装的 string 包含了 char* 数组、容量和长度等属性
  • string 可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容

37. C++ 的四种强制转换

  • static_cast
    • 没有运行时类型检查来保证转换的安全性
    • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
      • 进行上行转换(派生类的指针或引用 --> 基类)是安全的
      • 进行下行转换(基类的指针或引用 --> 派生类)时,由于没有动态类型检查,所以是不安全的
    • 用于基本数据类型之间的转换:int --> char,int --> enum,这种转换的安全性也要程序员来保证
    • 把空指针转换成目标类型的空指针
    • 把任何类型的表达式转换成 void 类型
    // 把 expression 转换为 type-id 类型
    static_cast <type-id> (expression)
    
  • dynamic_cast
    • dynamic_cast 具有类型检查的功能,比 static_cast 更安全
      • 基类向派生类转换(下行转换)比较安全
      • 派生类向基类转换(上行转换)不太安全
    • 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
    • 在类层次间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的
    // type-id 必须是类的指针、类的引用或者 void*
    dynamic_cast <type-id> (expression)
    
  • const_cast
    • 用来修改类型的 const 或 volatile 属性
    • 常量指针被转化成非常量的指针,并且仍然指向原来的对象
    • 常量引用被转换成非常量的引用,并且仍然指向原来的对象
    • const_cast 一般用于修改底指针,如 const char *p 形式
    const_cast<type_id> (expression)
    
  • reinterpret_cast
    • type-id 必须是一个指针、引用、算术类型、函数指针或成员指针,用于类型之间进行强制转换
    reinterpret_cast<type-id> (expression)
    

38. 全局变量和局部变量

  • 生命周期
    • 全局变量随主程序创建和创建,随主程序销毁而销毁
    • 局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在
  • 使用方式
    • 通过声明后全局变量在程序的各个部分都可以用到
    • 局部变量分配在堆栈区,只能在局部使用
  • 内存分配的位置
    • 全局变量分配在全局数据段并且在程序开始运行的时候被加载
    • 局部变量则分配在堆栈里面

非静态全局变量和静态全局变量

  • 都是采取静态存储方式(全局变量前再冠以 static 就构成静态全局变量)
  • 非静态的全局变量在各个源文件中都是有效的,而静态全局变量只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它,静态全局变量只初始化一次,防止在其他文件单元被引用

39. C++ 函数调用的压栈过程

  • 1、从栈空间分配存储空间
  • 2、从实参的存储空间复制值到形参栈空间
  • 3、进行运算
    #include <iostream>
    using namespace std;
    
    int f(int n) {
        cout << n << endl;
        return n;
    }
    
    void func(int param1, int param2) {
        int var1 = param1;
        int var2 = param2;
        // 如果将 printf 换为 cout 进行输出,输出结果则刚好相反
        printf("var1 = %d, var2 = %d", f(var1), f(var2));
    }
    
    int main(int argc, char* argv[]) {
        func(1, 2);
        return 0;
    }
    
    2
    1
    var1 = 1, var2 = 2
    
    • 当函数从入口函数 main 函数开始执行时,编译器会将操作系统的运行状态,main 函数的返回地址、main 的参数、mian 函数中的变量、进行依次压栈
    • 当 main 函数开始调用 func() 函数时,编译器此时会将 main 函数的运行状态进行压栈,再将 func() 函数的返回地址、func() 函数的参数从右到左、func() 定义变量依次压栈
    • 当 func() 调用 f() 的时候,编译器此时会将 func() 函数的运行状态进行压栈,再将 f() 的返回地址、f() 函数的参数从右到左、f() 定义变量依次压栈

40. C++ 的内存分区

  • 栈区(向下增长)

    • 存放函数的参数值、局部变量
    • 由编译器自动分配释放,栈内存分配运算内置于处理器的指令集中,效率很高,但分配的内存容量有限
  • 堆区(向上增长)

    • 由程序员分配和释放,若程序员不释放,程序结束时由操作系统自动回收
    • 由 new 关键字在堆区开辟内存,一个 new 就要对应一个 delete
  • 自由存储区

    • 堆是操作系统维护的一块内存,自由存储区是 C++ 中通过 new 和 delete 动态分配和释放对象的抽象概念
    • 自由存储区和堆比较像,但不等价
  • 全局/静态存储区

    • 存放全局变量和静态变量
    • C 语言中全局变量和静态变量又分为初始化的和未初始化的,C++ 则没有区分,它们共同占用同一块内存区,在该区定义的变量若没有初始化则会被自动初始化,例如 int 型变量自动初始为 0
  • 常量存储区

    • 存放常量,不允许修改
  • 代码区

    • 存放函数体的二进制代码,由操作系统进行管理的
      在这里插入图片描述
  • 程序运行前:在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

    • 代码区:存放 CPU 执行的机器指令
      • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
      • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
    • 全局区:
      • 全局变量和静态变量存放在此
      • 全局区还包含了常量区,字符串常量和其他常量也存放在此

      该区域的数据在程序结束后由操作系统释放

  • 程序运行后

    • 栈区(见上)
    • 堆区(见上)

C++ 内存的三种分配方式

  • 从静态存储区分配:此时的内存在程序编译的时候已经分配好,并且在程序的整个运行期间都存在。全局变量,static 变量等在此存储
  • 在栈区分配:相关代码执行时创建,执行结束时被自动释放。局部变量在此存储。栈内存分配运算内置于处理器的指令集中,效率高,但容量有限
  • 在堆区分配:动态分配内存。用 new/malloc 开辟,delete/free 释放。生存期由用户指定,灵活,但有内存泄露等问题
  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值