C++八股文--基础详解

目录

1. 智能指针

2. 内存分配

3. 指针参数传递和引用参数传递

4. 关键字static

5. 关键字const

7. 关键字define、const、typedef、inline

8. 重载、重写和重定义

9. 强制转换

10. 指针和引用

11. 野指针和悬空指针

12. 构造函数

13. 堆和栈

14. 函数传参

15. malloc/free和new/delete

16. volatile和extern

17. 类大小

18. 结构体内存对齐

19. 内存泄漏

20. 预处理、编译、汇编、链接

21. 命名空间

22. 模版

1. 智能指针

优点

(1)避免内存泄漏。底层维护一个类,作用域结束可以自动释放内存。

(2)共享指针便于传播和释放。例如多线程使用同一个对象时析构问题。

auto_ptr

(c++98用,c++11已放弃)。如下,p2剥夺了p1所有权,访问p1会报错。

auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1;

unique_ptr

独占所有权的指针,即同一时间只能有一个std::unique_ptr指向同一个对象。如下,P4 = P3会报错,因为P3不能再指向有效数据。

unique_ptr<string> p3 (new string (auto));
unique_ptr<string> p4;
p4 = p3;

shared_ptr

共享所有权的指针,即可以有多个std::shared_ptr指向同一个对象。有两个指针,一个指向堆内存中的对象,另一个指向共享实例的引用计数器。

引用计数器

  • 每次调用shared_ptr的复制构造函数时,引用计数器+1。
  • 每次调用shared_ptr的赋值运算符时,右侧指针计数器+1,左侧指针计数器-1。
  • 每次调用shared_ptr的析构函数时,计数器均-1。
  • 计数器为0时,删除该对象。

weak_ptr

弱引用指针,即一个std::weak_ptr指向一个std::shared_ptr所管理的对象,但不会增加引用计数。

2. 内存分配

栈:存放局部变量和函数参数,编译器管理内存分配和回收。

堆:程序员管理,手动new/delete或malloc/free,容易造成内存泄漏。

全局、静态存储区:存储初始化/未初始化的全局变量和静态变量,有初始化和未初始化两个区域。

代码区:存放二进制代码。

一维数组动态分配释放代码示例:

// 动态分配,数组长度为 m
int *array=new int [m];
 
//释放内存
delete [] array;

二维数组动态分配释放代码示例:

int **array;
array = new int *[m];
for(int i = 0; i < m; i++ ) {
    array[i] = new int [n];
}
//释放
for(int i = 0; i < m; i++ ) {
    delete [] array[i];
}
delete [] array;

对象动态分配释放代码示例:

#include <iostream>
using namespace std;
 
class Box {
   public:
      Box() { 
         cout << "调用构造函数!" <<endl; 
      }
      ~Box() { 
         cout << "调用析构函数!" <<endl; 
      }
};
 
int main( )
{
   Box* myBoxArray = new Box[4];
 
   delete [] myBoxArray;
   return 0;
}

3. 指针参数传递和引用参数传递

指针参数传递

本质是值传递,传递的是一个地址值,传递过程中,被调函数的形参作为被调函数的局部变量处理,在栈开辟空间存放有主调函数传递来的实参,从而形成实参副本。特点:被调函数对形参指针改变不影响主函数实参指针。

引用参数传递

被调函数形参也作为局部变量在栈中开辟了内存空间,存放的是主调函数放进来的实参变量的地址。特点:被调函数对形参指针改变会被处理成间接寻址,即通过栈中存储的地址找到实参,因此,被调函数对形参指针改变会影响主函数实参指针。

4. 关键字static

修饰局部变量

局部变量一般存储于栈,static修饰的局部变量存储于静态数据区,生命周期会一直延续到整个程序结束,作用域未变还是在其语句块。

修饰全局变量

static修饰的全局变量仅在本文件可见。

修饰函数

static修饰函数,函数仅在本文件可见。

修饰类

static修饰类成员变量,则对应成员变量属于整个类而不是对象,没有this指针(this指向本对象),需要在类外初始化,供所有对象共享节省内存,并且只能访问static修饰的类成员。

static修饰类成员方法,则对应成员方法属于整个类而不是对象,没有this指针,并且只能访问静态成员。

5. 关键字const

修饰基本数据类型:使用过程中不可改变常量值

修饰指针变量:const int* p1,指向整形常量。const int* const p3,指向整形常量的常量指针。

修饰引用变量:int* const p2,指向整形的常量指针。

修饰函数:

const int& function(int& x); //修饰返回值,返回值是引用,不能被修改
int& function(const int& x); //修饰形参
int& function(int& x) const{} //const成员函数,保证对象成员不被修改,可以访问非const和const的数据成员和变量,可以被重载; 如果成员是指针,则不能保证对象成员不被修改

7. 关键字define、const、typedef、inline

define:编译阶段处理,无类型,无类型检查,遇到就展开。

const:编译期间处理,有类型,有类型检查。const常量在出现的地方保留真正的数据,编译器有时不为const常量分配内存,直接将const常量添加到表中,省去读写内存,效率更高。

typedef:定义类型别名;与struct结合,定义类型;编译阶段有效,有类型检查;

inline:内联函数,调用时进行替换;有类型检查;

8. 重载、重写和重定义

重载:overload,函数同名,参数列表不同。

重写:override,子类重写父类中的虚函数(同名,同参数列表,同返回等)。

重定义:子类重新定义父类中同名非虚函数,可修改参数列表。

9. 强制转换

static_cast:转换类型明确;无动态类型检查;派生类转基类安全,所以主要适用于非多态。

dynamic_cast:动态派生类间转换;type id必须是类指针、类引用或void*;转换安全,类型不一致转换为空指针。

const_cast:专用于const类型去除或增加;

reinterpret_cast:整形转指针引用;指针转数组;指针引用间转换;指针引用转整形。

10. 指针和引用

指针是一个实体,引用是一个别名。

指针:独立的变量;有const;可以有多级;可为空;存放地址;编译时添加“指针变量名-指针变量地址”,允许拷贝和赋值;sizeof得到指针类型大小。

引用:依赖原变量的内存别名;无const;只有1级;编译时,将“引用变量名-引用对象地址”添加到符号表,之后从一而终,不能引用其他数据;sizeof得到所引用对象大小。

11. 野指针和悬空指针

野指针:没被初始化的指针。

悬空指针:指针指向的内存被回收,指针仍然指向这块内存。

12. 构造函数

创建类的对象时,编译器会为对象分配内存空间,自动调用构造函数初始化对象成员。

默认构造函数:一个类没有构造函数,生成默认构造函数条件为:有一个类类型成员变量,继承自含有默认构造函数的基类,继承或声明了虚函数,含有虚基类。

普通构造函数:创建新对象,初始化成员。

拷贝构造函数:用一个对象去初始化同类的另一个对象;函数F的参数是类A的对象,F被调用时候;函数F的参数返回值是类A的对象,F返回时。

转换构造函数:一个构造函数接受一个不同于其类类型的形参,可视为将形参转换成类的一个对象

移动构造函数:将其它对象拥有的资源内存移位己用。

赋值运算符重载:=右边的本类对象的值赋值给=左边的对象。

A a1, A a2; a1 = a2; // 赋值运算符重载
A a3 = a1; // 拷贝构造函数

13. 堆和栈

堆:程序员手动malloc和free或new和delete,如果不及时回收会造成内存泄漏;空间不连续;低地址向高地址扩展;空间较大较灵活;

栈:需要时编译器分配,不需要时自动回收;连续的存储空间;高地址向低地址扩展;空间较小。

14. 函数传参

值传递:形参是实参的拷贝。

指针传递:值传递的一种,形参指向实参地址。

引用传递:引用对象的地址放在栈空间。形参任何操作都可以直接映射到外部实参。

15. malloc/free和new/delete

区别:new/delete是操作符,可执行构造函数和析构函数;malloc/free是库函数,不在编译器控制权限之内。

new:分配未初始化的空间;用对象的构造函数对空间初始化。

delete:使用析构函数对对象析构;回收内存空间。

malloc:分配空间。

free:释放空间。

16. volatile和extern

volatile:易变性,下一条语句不回去用上一条语句里volatile变量的寄存器内容;不可优化型,不可优化和删除;顺序性,编译器不会进行重排乱序优化。

extern:C加此声明,表示此变量、函数在别处定义,在此引用;C++中除此之外还有一个规范性作用,即C++调用C库函数需要“extern C”。

17. 类大小

class A{}; sizeof(A) = 1;
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit); // 指向虚函数表
class A{static int a; }; sizeof(A) = 1;
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4;

18. 结构体内存对齐

结构体第一个成员偏移为0;之后每个成员偏移min(#pragma pack()指定数,数据成员本身长度);结构体本身也要进行对齐。

对齐原因:CPU内存访问速度提升;减少额外操作,举例开始存储1字节,读取0-3,读取4,删除0;CPU有时候拒绝处理未内存对齐的数据。

19. 内存泄漏

申请一块内存空间,使用完毕后未释放,但是指向此空间的指针已被回收,长期以往,这样的空间越来越多,再申请新空间空间不够时,系统崩溃。

20. 预处理、编译、汇编、链接

预处理:删除#define,展开宏定义,处理所有条件编译指令,处理所有预编译指令,删除所有注释,添加行号和文件名标识。

编译:源码转换为机器语言,转化为相应的汇编文件。静态编译:编译时候将对应链接库中的函数链接到可执行文件。动态链接:执行时,调用对应的动态链接库的命令。

汇编:调用汇编器将汇编代码转换为机器可执行的指令。

链接:将所有.o和库链接在一起,得到可执行文件。

21. 命名空间

命名空间定义:作为附加信息区分不同库中相同名称的函数、类、变量等。

代码示例:

#include <iostream>
using namespace std;
 
// 第一个命名空间
namespace first_space{
   void func() {
      cout << "Inside first_space" << endl;
   }
}
// 第二个命名空间
namespace second_space{
   void func() {
      cout << "Inside second_space" << endl;
   }
}
using namespace first_space;
int main () {
   func();
   return 0;
}

22. 模版

模板:创建泛型编程类或函数。

代码示例:

#include <iostream>
#include <string>
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) { 
    return a < b ? b:a; 
} 
int main () {
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
    return 0;
}

类模板代码示例:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept> 
using namespace std;
 
template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty(); 
    } 
}; 
 
template <class T>
void Stack<T>::push (T const& elem) { 
    elems.push_back(elem);    
} 
 
template <class T>
void Stack<T>::pop () { 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    elems.pop_back();         
} 
 
template <class T>
T Stack<T>::top () const { 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::top(): empty stack"); 
    }
    // 返回最后一个元素
    return elems.back();      
} 
 
int main() { 
    try { 
        Stack<int>         intStack;  // int 类型的栈 
        Stack<string> stringStack;    // string 类型的栈 
 
        intStack.push(7); 
        cout << intStack.top() <<endl; 
 
        stringStack.push("hello"); 
        cout << stringStack.top() << std::endl; 
        stringStack.pop(); 
        stringStack.pop(); 
    } 
    catch (exception const& ex) { 
        cerr << "Exception: " << ex.what() <<endl; 
        return -1;
    } 
}