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;
}
- 注意事项
- 使用
auto
关键字的变量必须有初始值。 - 可以使用
valatile
,*
(指针类型说明符),&
(引用类型说明符),&&
(右值引用)来修饰auto
关键字。 - 函数参数和模板参数不能被声明为auto。
- 使用
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变量
- 对于数组类型,
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
变量的几个例子:- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量
- 多线程应用中被几个任务共享的变量
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
的值可能在两次取值语句之间发生改变,因此a
和b
可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
long square(volatile int*ptr) {
int a;
a = *ptr;
return a*a;
}
- 关于编译器的优化
- 在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;
- 当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致;
- 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致; 链接🔗
引用和指针
引用只是c++语法糖,可以看作编译器自动完成取地址、解引用的常量指针,二者在汇编层面甚至没什么区别。
不同点:
- 引用在创建时必须初始化,引用到一个有效对象;而指针在定义时不必初始化,可以在定义后的任何地方重新赋值。
- 指针可以是
NULL
,引用不行。 - 指针和引用的自增(
++
)和自减(--
)含义不同,指针是内存地址运算, 而引用是代表所指向的对象对象执行++
或--
。
C++程序内存
C/C++不提供垃圾回收机制,因此需要对堆中的数据进行及时销毁,防止内存泄漏,使用free
和delete
销毁new
和malloc
申请的堆内存,而栈内存是动态释放。
C语言在内存中一共分为如下几个区域,分别是:
- 栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
- 堆区:由new分配的内存块,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
- 全局/静态存储区:全局变量和静态变量的存储是放在一块的,程序结束后由系统释放 。
- 文字常量区:常量字符串就是放在这里的。程序结束后由系统释放(RO)
- 程序代码区:存放函数体的二进制代码。(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;
}
堆和栈的区别
- 管理方式不同:栈是由编译器自动管理,无需我们手工控制;对于堆来说,释放由程序员完成,容易产生内存泄漏。
- 空间大小不同:一般来讲,在32为系统下面,堆内存可达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的,例如,在vc6下面,默认的栈大小好像是1M。
- 能否产生碎片:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。
- 生长方向不同:堆生长方向是向上的,也就是向着内存地址增加的方向;栈的生长方式是向下的,是向着内存地址减小的方向增长。
- 分配方式不同:堆都是动态分配的;栈有静态和动态两种分配方式。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca函数进行、但栈的动态分配和堆是不同的,它的动态分配由编译器进行释放,无需我们手工实现。
- 分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是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
,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。 [附加]🔗
多态 🔗
- 函数重载的规则:
- 函数名称必须相同。
- 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载。
- 泛型编程
- 函数模板 动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
- 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
- 通过基类类型的指针或引用来调用虚函数。
- 「派生类的指针」可以赋给「基类指针」
- 通过基类指针调用基类和派生类中的同名「虚函数」时:
- 若该指针指向一个基类的对象,那么被调用是基类的虚函数;
- 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。 这种机制就叫做“多态”,说白点就是调用哪个虚函数,取决于指针对象指向哪种类型的对象。
- 派生类的对象可以赋给基类「引用」
- 通过基类引用调用基类和派生类中的同名「虚函数」时:
- 若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
- 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。 这种机制也叫做“多态”,说白点就是调用哪个虚函数,取决于引用的对象是哪种类型的对象。
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时,则自动删除这个对象。
- 标记清除
- 节点复制
- 分代回收