c++基础

604 阅读11分钟

c++基础

vector扩容原理说明

  • 新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素;
  • vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
  • 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
iterator start;  // 表示目前使用空间的头
iterator finish;  // 表示目前使用空间的尾
iterator end_of_storage;  // 表示目前可用空间的尾

通过这三个迭代器,就可以实现我们想要的很多操作,比如提供首尾标示,大小,容量,空容器判断,[ ]运算符,最前端元素,最后端元素等
vector删除元素remove与erase区别

explicit关键字

  • C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的。跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
#include <iostream>
using namespace std;

class Test{
public:
    int _num;
    explicit Test(int num): _num(num) {} 
};

int main()
{
    Test t1(3);  // OK
    Test t2 = 3;  // ❌错误,因为explicit关键字取消了隐式转换
    return 0;
}
  • explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。
  • 但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。

auto关键字

  • C++引入auto关键字主要有两种用途:一是在变量声明时根据初始化表达式自动推断该变量的类型,二是在声明函数时作为函数返回值的占位符(对于自动类型推断,C++11中也有一个类似的关键字decltype)。
  • 对于第二种情况:
template<class T, class U>
auto add(T t, U u) -> decltype(t + u) 
{
    return t + u;
}
  • 注意事项
  1. 使用auto关键字的变量必须有初始值。
  2. 可以使用valatile*(指针类型说明符),&(引用类型说明符),&&(右值引用)来修饰auto关键字。
  3. 函数参数和模板参数不能被声明为auto。
  4. 使用auto关键字声明变量的类型,不能自动推导出顶层的CV-qualifiers和引用类型,除非显示声明。例如:
    • 使用auto关键字进行类型推导时,如果初始化表达式是引用类型,编译器会去除引用,除非显示声明。例如:
    int i = 10;
    int &r = i;
    auto a = r;  // ≠ int &a = r;
    a = 13;   // 重新赋值
    cout << "i = " << i << " a = " << a << endl;    // 输出i=10,a=13
    
    // 显式声明
    auto &b = r;
    b = 15; // 重新赋值
    cout << "i = " << i << " b = " << b << endl;    // 输出i=15,a=15
    
    • 使用auto使用auto关键字进行类型推导时,编译器会自动忽略顶层const,除非显示声明。例如:
    const int c1 = 10;
    auto c2 = c1;
    c1 = 11;  // ❌报错,c1为const int类型,无法修改const变量
    c2 = 14;  // 正确,c2为int类型
    
    // 显示声明
    const auto c3 = c1;
    c3 = 15;   // ❌报错,c3为const int类型,无法修改const变量
    
    
  5. 对于数组类型,auto关键字会推导为指针类型,除非被声明为引用。
    int a[10];
    auto b = a;
    cout << typeid(b).name() << endl;   // 输出:int *
 
    auto &c = a;
    cout << typeid(c).name() << endl;   // 输出:int [10]

volatitle关键字

  • volatile是一个类型修饰符(就像我们熟悉的const一样),它是被设计用来修饰被不同线程访问和修改的变量volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
  • 精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。 下面是volatile变量的几个例子:
    1. 并行设备的硬件寄存器(如:状态寄存器)
    2. 一个中断服务子程序中会访问到的非自动变量
    3. 多线程应用中被几个任务共享的变量
int square(volatile int *ptr) {
    return ((*ptr) * (*ptr));
}

这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int* &ptr) {//这里参数应该申明为引用,不然函数体里只会使用副本,外部没法更改
    int a, b;
    a = *ptr;
    b = *ptr;
    return a*b;
}

由于*ptr的值可能在两次取值语句之间发生改变,因此ab可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:

long square(volatile int*ptr) {
    int a;
    a = *ptr;
    return a*a;
}
  • 关于编译器的优化
    • 在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;
    • 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致;
    • 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致; 链接🔗

引用和指针

引用只是c++语法糖,可以看作编译器自动完成取地址、解引用的常量指针,二者在汇编层面甚至没什么区别。
不同点:

  • 引用在创建时必须初始化,引用到一个有效对象;而指针在定义时不必初始化,可以在定义后的任何地方重新赋值。
  • 指针可以是NULL,引用不行。
  • 指针和引用的自增(++)和自减(--)含义不同,指针是内存地址运算, 而引用是代表所指向的对象对象执行++--

C++程序内存

C/C++不提供垃圾回收机制,因此需要对堆中的数据进行及时销毁,防止内存泄漏,使用freedelete销毁newmalloc申请的堆内存,而栈内存是动态释放。

C语言在内存中一共分为如下几个区域,分别是:

  1. 栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
  2. 堆区:由new分配的内存块,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
  3. 全局/静态存储区:全局变量和静态变量的存储是放在一块的,程序结束后由系统释放 。
  4. 文字常量区:常量字符串就是放在这里的。程序结束后由系统释放(RO)
  5. 程序代码区:存放函数体的二进制代码。(RO)
int a = 0;  // 全局(初始化)区 
char *p1;   // 全局(未初始化)区 
int main() 
{ 
int b;  // 栈 
char s[] = "abc";  // "abc"在常量区,s在栈区 
char *p2; //栈 
char *p3 = "123456"; //123456\0";在常量区,p3在栈上。 

static int c =0;  // 全局(初始化)区 

p1 = (char *)malloc(10); 
p2 = (char *)malloc(20);  
//分配得来得10和20字节的区域就在堆区。 
 
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
return 0;
}

堆和栈的区别

  1. 管理方式不同:栈是由编译器自动管理,无需我们手工控制;对于堆来说,释放由程序员完成,容易产生内存泄漏。
  2. 空间大小不同:一般来讲,在32为系统下面,堆内存可达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的,例如,在vc6下面,默认的栈大小好像是1M。
  3. 能否产生碎片:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。
  4. 生长方向不同:堆生长方向是向上的,也就是向着内存地址增加的方向;栈的生长方式是向下的,是向着内存地址减小的方向增长。
  5. 分配方式不同:堆都是动态分配的;栈有静态和动态两种分配方式。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca函数进行、但栈的动态分配和堆是不同的,它的动态分配由编译器进行释放,无需我们手工实现。
  6. 分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是c/c++库函数提供的,机制很复杂。库函数会按照一定的算法进行分配。显然,堆的效率比栈要低得多。

C++构造函数初始化列表

构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表

  • 使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。
  • 成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
  • 初始化 const 成员变量的唯一方法就是使用初始化列表。

this指针

  • 在 C++ 中,每一个对象都能通过this指针来访问自己的地址this指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
  • 友元函数没有this指针,因为友元不是类的成员。只有成员函数才有this指针。
class Box {
    private:
        double length;      // Length of a box
        double breadth;   // Breadth of a box
        double height;     // Height of a box
   public:
      // 构造函数定义
      Box(double l=2.0, double b=2.0, double h=2.0): length(l), breadth(b), height(h) {}
      double Volume() {
         return length * breadth * height;
      }
      int compare(Box box){
         return this->Volume() > box.Volume();
      }
};

c++封装、继承、多态

面向对象的三个基本特征是:封装继承多态

封装

  • 数据封装是一种把数据和操作数据的函数捆绑在一起的机制。
  • C++ 通过创建来支持封装。

继承

多继承(环状继承),例如:

class D {......};
class B: public D {......};
class A: public D {......};
class C: public B, public A {.....};

这个继承会使D创建两个对象,要解决上面问题就要用虚继承格式:class 类名: virtual 继承方式 父类名

class D {......};
class B: virtual public D {......};
class A: virtual public D {......};
class C: public B, public A {.....};

虚表 vTable(虚表是指针数组,每个元素都是一个虚函数的函数指针,指向类中对应的虚函数)

虚继承、动态绑定的底层实现便是基于虚表,虚指针的。

  • 函数只要有virtual,我们就需要把它添加进vTable

  • 虚表是一个指针数组,其元素是虚函数的指针,每个虚表元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表。

  • 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

  • 虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表指针(对象通过虚表指针来指向自己的虚表)

  • 当一个类A继承另一个类B时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数。所以,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

  • 为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。 [附加]🔗

多态 🔗

多态{静态多态{函数重载范型编程动态多态虚函数多态\left\{ \begin{aligned} &静态多态 \left\{\begin{aligned} &函数重载\\ &范型编程 \end{aligned} \right. \\ &动态多态 - 虚函数 \end{aligned} \right.

  • 函数重载的规则:
    • 函数名称必须相同。
    • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
    • 函数的返回类型可以相同也可以不相同。
    • 仅仅返回类型不同不足以成为函数的重载。
  • 泛型编程
    • 函数模板 动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
    1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
    2. 通过基类类型的指针或引用来调用虚函数。
  1. 「派生类的指针」可以赋给「基类指针」
  • 通过基类指针调用基类和派生类中的同名「虚函数」时:
    • 若该指针指向一个基类的对象,那么被调用是基类的虚函数;
    • 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。 这种机制就叫做“多态”,说白点就是调用哪个虚函数,取决于指针对象指向哪种类型的对象。
  1. 派生类的对象可以赋给基类「引用」
  • 通过基类引用调用基类和派生类中的同名「虚函数」时:
    • 若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
    • 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。 这种机制也叫做“多态”,说白点就是调用哪个虚函数,取决于引用的对象是哪种类型的对象。

c++11新特征

  • “语法糖”:nullptr,auto自动类型推导,decltype关键字,编译期推导表达式类型,范围for循环for (int& e: arr),初始化参数列表,lambda表达式等
  • 右值引用&&和移动语义move()(将左值引用转换为右值引用)
  • 智能指针
  • 新增”散列容器“:unordered_map/unordered_set和元组tuple
  • bug的修复:>>中间不需要加空格了,如vector<vector<int>> a;

c++虚析构函数 🔗

c++右值引用

C++垃圾回收算法 🔗

  • 引用计数: 为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。
  • 标记清除
  • 节点复制
  • 分代回收