job

Job-Oriented C++

C++面向八股学习

Posted by Kylin on April 13, 2023

[TOC]

库文件

  • .dll

DLL代表“Dynamic Link Library”,它是Windows操作系统中的一种共享库文件(可重用二进制代码和资源)。这些文件包含计算机程序可以在运行时调用的可执行代码、数据和资源。

  • .a 和 .so

“.so”文件,也称为共享库文件,是一种动态链接库文件。它包含了可在运行时加载的函数和变量的代码和数据。由于多个程序可以共享同一个”.so”文件,因此它可以帮助节省内存空间,并且可以简化软件开发和维护。

“.a”文件,也称为静态库文件,是一种包含可重用函数和变量代码的归档文件。在链接时,程序将这些文件中的代码复制到可执行文件中,因此静态库文件在执行时不需要动态加载。由于静态库文件的代码被复制到每个可执行文件中,因此静态链接库文件可能会导致可执行文件变得更大,并且在更新库时需要重新编译所有使用该库的程序。

  • ELF 文件格式

可执行与可链接格式 (Executable and Linkable Format) 是一种用于可执行文件、目标代码、共享库和核心转储 (core dump) 的标准文件格式,每个 ELF 文件都由一个 ELF header 和紧跟其后的文件数据部分组成,可以参考 ELF 文件的构成如下:

1661172875-aSnoQn-1_2_1

  • .text section:代码段。通常存放已编译程序的机器代码,一般操作系统加载后,这部分是只读的。

  • .rodatasection:只读数据段。此段的数据不可修改,存放程序中会使用的常量。比如程序中的常量字符串”aasdasdaaasdasd”。

  • .datasection:数据段。主要用于存放已初始化的全局变量、常量。

  • .bsssection: bss 段。该段主要存储未初始化全局变量,仅是占位符,不占据任何实际磁盘空间。目标文件格式区分初始化和非初始化是为了空间效率。

操作系统在加载 ELF 文件时会将按照标准依次读取每个段中的内容,并将其加载到内存中,同时为该进程分配栈空间,并将 pc 寄存器指向代码段的起始位置,然后启动进程。

内存分区

C++ 程序在运行时也会按照不同的功能划分不同的段,C++ 程序使用的内存分区一般包括:栈、堆、全局/静态存储区、常量存储区、代码区。

  • 栈:目前绝大部分 CPU 体系都是基于栈来运行程序,栈中主要存放函数的局部变量、函数参数、返回地址等,栈空间一般由操作系统进行默认分配或者程序指定分配,栈空间在进程生存周期一直都存在,当进程退出时,操作系统才会对栈空间进行回收。

  • 堆:动态申请的内存空间,就是由 malloc 函数或者 new 函数分配的内存块,由程序控制它的分配和释放,可以在程序运行周期内随时进行申请和释放,如果进程结束后还没有释放,操作系统会自动回收。

  • 全局区/静态存储区:主要为 .bss 段和 .data 段,存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

  • 常量存储区:.rodata 段,存放的是常量,不允许修改,程序运行结束自动释放。

  • 代码区:.text 段,存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

#include <iostream>
using namespace std;
/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/
int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var;  // gp_var 在全局区(.bss 段)

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区(.data 段)
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

变量

全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。

静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回,局部变量对于函数外部的程序来说是不可见的。当然内部实际更复杂,实际是以 {} 为作用域的。

静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见, 只有定义该变量的函数内部可以使用访问和修改该变量。 比如以下文件定义

储存位置
  • 静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段,bss 段,rodata 段)中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
  • 静态变量和全局变量的区别:静态变量仅在变量的作用范围内可见,实际是依靠编译器来控制作用域。全局变量在整个程序范围内都可可见,只需声明该全局变量,即可使用。
  • 全局变量定义在不要在头文件中定义:如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,编译时会因为重复定义而报错,因此不能再头文件中定义全局变量。一般情况下我们将变量的定义放在 .cpp 文件中,一般在 .h 文件使用extern 对变量进行声明。

1661412780-mtAvuQ-1_2_2

内存泄漏

程序在堆中申请的动态内存,在程序使用完成时没有得到及时的释放。当这些变量的生命周期已结束时,该变量在堆中所占用的内存未能得到释放,从而就导致了堆中可使用的内存越来越少,最终可能产生系统运行较慢或者系统因内存不足而崩溃的问题。 内存泄漏并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。 内存泄漏主要指堆上分配的变量,因为栈中分配的变量,随着函数退出时会自动回收。而堆是动态分配的,一旦用户申请了内存分配而为及时释放,那么该部分内存在整个程序运行周期内都是被占用的,其他程序无法再使用这部分内存。 对于实际的程序来说,我们在调用过程中使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete 释放内存,否则这块内存就会造成内存泄漏。

智能指针

在C++11之后,引入了智能指针(smart pointers)的概念,可以用来自动管理内存。智能指针是一种封装了原始指针的对象,它可以在对象生命周期结束时自动释放分配的内存。使用智能指针可以避免手动释放内存的繁琐和容易出错的问题,同时提高程序的可靠性和安全性。

C++ 类对象的初始化顺序

  • 构造函数调用顺序:
    • 按照派生类继承基类的顺序,即派生列表中声明的继承顺序,依次调用基类的构造函数;
    • 在有虚继承和一般继承存在的情况下,优先虚继承。比如虚继承:class C: public B, virtual public A,此时应当先调用 A 的构造函数,再调用 B 的构造函数。
    • 按照派生类中成员变量的声明顺序,依次调用派生类中成员变量所属类的构造函数; 执行派生类自身的构造函数。
  • 类对象的初始化顺序:
    • 按照构造函数的调用顺序,调用基类的构造函数
    • 按照成员变量的声明顺序,调用成员变量的构造函数函数,成员变量的初始化顺序与声明顺序有关; 调用该类自身的构造函数;
    • 析构顺序和类对象的初始化顺序相反。
#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
};

class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
};

class Test : public A, public B // 派生列表
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }

private:
    B ex1;
    A ex2;
};

int main()
{
    Test ex;
    return 0;
}
/*
运行结果:
A()
B()
B()
A()
Test()
~Test()
~A()
~B()
~B()
~A()
*/

程序运行结果分析:

首先调用基类 A 和 B 的构造函数,按照派生列表 public A, public B 的顺序构造; 然后调用派生类 Test 的成员变量 ex1 和 ex2 的构造函数,按照派生类中成员变量声明的顺序构造; 最后调用派生类的构造函数; 接下来调用析构函数,和构造函数调用的顺序相反。

  • 类的成员初始化:
    • 类中可能含有静态变量和全局变量,由于静态变量和全局变量都被放在静态存储区,他们的初始化在 main 函数执行之前已被初始化,且 static 变量必须在类外进行初始化。
    • 成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。
    • 如果类不使用初始化列表初始化,而在类的构造函数内部进行初始化时,此时成员变量的初始化顺序与构造函数中代码逻辑有关。
    • 类成员在定义时,是不能初始化的
    • 类中 const 成员常量必须在构造函数初始化列表中初始化。
    • 类中 static 成员变量,必须在类外初始化。

左值与右值

  • 左值:指表达式结束后依然存在的持久对象。可以取地址,可以通过内置(不包含重载) & 来获取地址,我们可以将一个右值赋给左值。
  • 右值:表达式结束就不再存在的临时对象。不可取地址,不可以通过内置(不包含重载) & 来获取地址。由于右值不可取地址,因此我们不能将任何值赋给右值。
  • 使用 = 进行赋值时,= 的左边必须为左值,右值只能出现在 = 的右边
// x 是左值,666 为右值
int x = 666;   // ok 
int *y = x; // ok
int *z = &666 // error
666 = x; // error
int a = 9; // a 为左值
int b = 4; // b 为左值
int c = a + b // c 为左值 , a + b 为右值
a + b = 42; // error
  • 函数返回值即可以是左值,也可以是右值:
int setValue()
{
    return 6;
}

int global = 100;

int& setGlobal()
{
    return global;    
}
setValue() = 3; // error!
setGlobal() = 400; // OK