本文章结合许多互联网中的数据。所谓取之互联网,用之互联网。若涉及版权侵犯,可以留言或者私信告知。
目前还有很多知识更新中,有什么错误欢迎评论交流。
定义全局静态变量
定义局部静态变量
定义静态函数
定义类的静态成员
静态成员是类的所有对象共享的成员。
定义类的静态函数
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数要引用非静态成员,可以通过对象来引用。(<类名>::<静态成员函数名>(<参数表>))
这是一个超链接
这是一个超链接
封装:将具体的数据和实现过程封装为类,只能通过接口访问,降低耦合性。
耦合性:是指一程序中,模块及模块之间信息或参数依赖的程度。
继承:子类继承父类的行为和属性,子类拥有父类中非私有的变量和函数。子类可以重写父类中的方法,增强了耦合性。被final修饰的类不能被继承,被final修饰的成员不能被重写或修改。
多态:不同子类对同一个消息,作出不同的反应。基类的指针指向子类的对象,使得基类指针作出不同的应答。
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定函数地址;动态多态是用虚函数机制实现的,在运行期间动态绑定。
C++的多态分为两类,一个是编译期多态,一个是运行期多态。
编译期多态主要是通过模板类来实现的。
运行期的多态主要是通过虚函数结合动态绑定实现的
这是一个超链接
子类重写父类虚函数,虚函数表中,该函数的地址直接替换父类虚函数在虚表中的位置。访问虚函数表,首先取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl,然后就可根据调用的虚函数取出slot存放的函数地址,最后调用这个函数。对象模型的头部一般存放虚函数表的指针,通过该机制实现多态。
只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。
在有虚函数的类中,类最前面的位置是一个虚函数表的指针vptr,这个指针指向一个虚函数表,表中存放所有虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承父类,也会继承其虚函数表,当子类重写父类中的虚函数时,会将其继承到的虚函数表中的地址替换为重写的函数地址。每个对象中的vptr都是独立一份的,且在该类对象被构造时被初始化。使用虚函数,会增加访问内存开销,降低效率。
虚函数表在编译期生成,存放在全局数据区,即静态区。
虚继承是用来解决多重继承中的命名冲突和数据冗余问题。比如类C继承类B1、类B2,而类B1和类B2都是继承了A,那么在C中就有两份类A里的数据,虚继承就是,B1和B2对A都是虚继承,那么A就是虚基类。这样在派生类C中就只有一份类A中的数据。
虚继承的目的就是让类声明,愿意共享基类,这个基类就是虚基类
设计思想上
C++是面向对象的语言,C是面向过程的结构化编程语言
语法上
C++具有重载、继承和多态三种特性
C++相比C,增加许多类型安全的功能,比如强制类型转换
C++支持范式编程,比如模板类、函数模板类等
const_cast
用于将const变量转为非const
static_cast
用于各种隐式转换,比如非const转为const,void*转指针等
用于多态向上转化,向下转能成功但是不安全,并且结果未知
比如short->int,int->double风险较低
dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上(和向下)转换。只能转指针或引用类型。对于指针,如果转换失败将返回NULL,对于引用,如果转换失败将抛出std::bad_cast
异常。
向下转换:指的是子类向基类的转换
向上转换:指的是基类向子类的转换
它通过判断执行到该语句时,变量的运行类型和要转换的类型是否相同来判断是否能够进行向下转换。
reinterpret_cast
仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,几乎什么都可以转,比如将int转指针,所以风险很高。
为什么不使用C的强制转换?
转换不明确,不能进行错误检查,容易出错。
const_cast <new_type>(expression) static_cast <new_type>(expression) dynamic_cast <new_type>(expression) reinterpret_cast <new_type>(expression) const int constant = 21; int* modifier = const_cast<int*>(&constant); *modifier = 7; /* constant = 21 modifier = 7 指向同一个地址 */
四个智能指针:auto_ptr,shared_ptr,weak_ptr,unique_ptr。后三个是C++11支持的,第一个已经被C++11弃用了。
auto_ptr(C++98的方案,C++11已经抛弃)
采用所有权模式。
auto_ptr<string>p1 (new string("I reigned lonely as a cloud.")); auto_ptr<string>p2; p2 = p1;//不会报错
p2剥夺了p1的所有权,当程序运行时访问p1将会报错,这就展现出auto_ptr的缺点:存在潜在的内存崩溃问题!
unique_ptr(替换auto_ptr)
unique_ptr是独占资源所有权的指针,保证同一时间内只有一个智能指针可以指向该对象。对于避免资源泄漏特别有用。(new创建对象后因为发生异常没有调用delete)
采用所有权模式。
unique_ptr<string>p3 (new string("I reigned lonely as a cloud.")); unique_ptr<string>p4; p4 = p3;//会报错
认为p4=p3非法,避免p3不再指向有效数据的问题。所以unique_ptr比auto_ptr更安全。
当程序试图将一个unique_ptr赋值给另一个,如果源unique_ptr是个临时右值,允许;如果源unique_ptr将存在一段时间,禁止。
unique_ptr<string>pu1 (new string("hello")); unique_ptr<string>pu2; pu2 = pu1;//#1 not allowed unique_ptr<string>pu3; pu3 = unique_ptr<string>(new string("world"));//#2 allowed
#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它会调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给pu3后会被销毁。
如果想执行#1的操作,要安全的重用这种指针,可以给它赋新值。C++中有一个标准库函数std::move(),能够将一个unique_ptr赋给另一个。
unique_ptr<string>p1,p2; p1 = unique_ptr<string>(new string("hello")); p2 = move(p1);//此时p1未赋值,只是转移,没有任何拷贝的意思 p1 = unique_ptr<string>(new string("world")); cout<<*p1<< ' '<<*p2<<'\n';//输出:hello world
shared_ptr
shared_ptr是共享资源所有权的指针。多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时释放。使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr,shared_ptr来构造。当我们调用reset()时,当前指针会释放资源所有权,计数减一。当计数=0时资源会被释放。
shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
user_count 返回引用计数的个数
unique 返回是否独占所有权(user_count=1)
swap 交换两个shared_ptr对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少
get 返回内部对象(指针),由于已经重载了方法,因此和直接适用对象是一样的,如shared_ptrsp(new int(1));sp与sp.get()是等价的。
shared_ptr<string>p1(new string("hello")); shared_ptr<string>p2(p1); cout<<*p1.get()<<'\n'; cout<<p1.use_count()<<'\n'; p1.reset(); cout<<p2.use_count()<<'\n'; if(p2.unique())cout<<"lonely"<<'\n'; else cout<<"not lonely"<<'\n'; /* hello 2 1 lonely */
weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理是那个强引用的 shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段,可以通过传入shared_ptr或weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。
成员函数
weak_ptr没有重载*和->,但可以使用lock获得一个可用的shared_ptr对象(weak_ptr在使用前需要检查合法性)
expired 用于检测所管理的对象是否已经释放,如果已经释放返回true,否则返回false
lock 用于获取所管理对象的强引用(shared_ptr),如果对象存在返回以恶搞shared_ptr,其内部对象指向与weak_ptr相同,否则返回一个空的shared_ptr。
reset weak_ptr置空
weak_ptr支持拷贝和赋值,但不会影响shared_ptr的计数
class Person{ private: string name; shared_ptr<Person>partner;//weak_ptr<Person>partner public: Person(const string& _name):name(_name){ cout<<name<<" created"<<'\n'; } virtual ~Person(){ cout<<name<<" destroyed"<<'\n'; } friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2) { if (!p1 || !p2) { return false; } p1->partner = p2; // weak_ptr重载的赋值运算符中可以接收shared_ptr对象 p2->partner = p1; cout << p1->name << " is now partenered with " << p2->name << endl; return true; } }; int main(){ auto p1 = std::make_shared<Person>("Lucy"); auto p2 = std::make_shared<Person>("Ricky"); partnerUp(p1, p2); // 互相设为伙伴 } /* Lucy created Ricky created Lucy is now partenered with Ricky shared_ptr<Person>partner->weak_ptr<Person>partner Lucy created Ricky created Lucy is now partenered with Ricky Ricky destroyed Lucy destroyed */
p1和p2相互引用,两个资源引用计数为2,跳出函数计数都减一,但还是不等于0,资源无法被释放。可以将成员属性改为weak_ptr,这样互相引用不会影响计数,最后跳出函数计数为0,资源可以被释放。
为什么要是有智能指针?
智能指针的作用是管理一个指针。因为可能存在申请的空间函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上避免这种问题,因为智能指针就是一个类,当超出类的作用域,类会自动调用析构函数释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间。
智能指针基于RAII(Resource Acquisition Is Initialization),称为"资源获取就是初始化",是一种利用对象生命周期控制程序资源的技术。
当两个对象互相使用一个shared_ptr成员指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
为了解决循环引用引进了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,就类似一个普通指针,但不指向引用计数的共享内存,可以检测所管理的对象是否已经被释放,从而避免非法访问。
指针 | 数组 |
---|---|
保存数据的地址 | 保存数据 |
间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据 | 直接访问数据 |
通常用于动态的数据结构 | 通常用于固定数目且数据类型相同的元素 |
通过malloc分配内存,free释放内存 | 隐式的分配和删除 |
通常指向匿名数据,操作匿名函数 | 自身即为数据名 |
野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针。
作用:给变量取别名
语法:数据类型 &别名 = 原名
在c++内部实现是一个指针常量。
这是为了防止内存泄漏。如果不定义成虚函数,那么只会调用基类的析构函数,等于只释放了基类的内存,而没有释放掉派生类的内存,定义为虚函数后,会先调用派生类的析构函数,然后调用基类的析构函数。
因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。对于不会被继承的类,使用虚函数就是浪费内存。
每一个拥有虚函数的类对象都有一个指向虚函数表的指针。对象通过虚函数表里存储的虚函数指针来调用虚函数。而虚函数表指针是在调用构造函数的时候初始化的,等于在调用构造函数的时候,此时虚函数表指针还不存在,找不到对应虚函数指针,所以不能将构造函数定义为虚函数。
这是一个超链接
语法上没错,但是没有调用的必要。
对于构造函数,基类调用了虚函数,此时派生类的虚函数还没构造好,因为派生类肯定比基类晚构造,所以你调用的虚函数就是基类里面的,这样就没有实现多态。
同理,析构的话肯定是派生类先析构,那么此时你基类的析构函数调用虚函数,派生类都被析构完了,调用的也只是基类内的函数而已,也没有实现多态。
设计者可能希望在基类指针指向子类对象时,通过该基类指针调用的虚函数版本应该是子类的虚函数版本。结果不能达到需要的目的。设为普通函数照样可以达到这个效果。
一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
int *fun(int x,int y); int * fun(int x,int y); int* fun(int x,int y);
其本质是一个指针变量,该指针指向这个函数。即函数指针就是指向函数的指针。
//函数 int add(int x,int y){ return x+y; } //函数指针 int (*fun)(int x,int y); int main(){ fun = add;//or fun = &add cout<<fun(1,2)<<'\n'; cout<<(*fun)(3,4)<<'\n';//更明显看出这是一个指针 return 0; }
对象的初始化和清理是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供空实现的构造函数和析构函数。
构造函数语法:类名(){}
析构函数语法: ~类名(){}
静态函数在编译时就已经确定运行时机(函数地址),虚函数在运行时动态绑定。虚函数因为用了虚函数表机制,调用时会增加一次内存开销。
重载:同一个作用域下,函数名称相同,函数参数类型、个数或者顺序不同。
重写:不同类之间,父类与子类,函数返回值类型、函数名、参数列表完全一致称为重写。父类中被重写的函数需要用virtual修饰。
++i先自增1,再返回i
int& int::operator++(){ *this += 1; return *this; }
i++先返回i,再自增1
const int int::operator(int){ int oldValue = *this; ++(*this); return oldValue; }
#include <stdio.h> __attribute((constructor)) void before_main() { printf("%s\n",__FUNCTION__); } __attribute((destructor)) void after_main() { printf("%s\n",__FUNCTION__); } int main( int argc, char ** argv ) { printf("%s\n",__FUNCTION__); return 0; } /* before_main main after_main */
使用define预处理器或者const关键字定义常量
#define CLK_TCK 10 const int CLK_TCK = 10;
常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量(常数),常量存放在常量存储区。
const修饰的成员函数表明函数调用不会对对象做出任何更改。事实上如果确认不会对对象做更改,就应该为函数加上const限定,这样无论是const对象还是普通对象都可以调用该函数。
如果是两个函数一个带const,一个不带,这相当于函数的重载
对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。
class Complex{ private: double real; double image; public: Complex(double _real,double _image):real(_real),image(_image){} //转换构造函数 Complex(double _real):real(_real),image(0){} //隐式转换函数 operator double(){ return real; } } int main(){ Complex c1(1.2,3.5); complex c2 = 4.4 double d = 1.8 + c1; }
只有一个形参的构造函数 Complex(double real)
就是转换构造函数,编译器需要将4.4转换为Complex
类型,于是以4.4为形参,调用Complex(double real)
构造函数,生成临时对象,然后再调用默认拷贝构造函数为对象c2进行初始化。
默认是1M,不过可以调整。
为了能够正确实现c++代码调用其他c语言代码。加上“extern C”后,会指示编译器这部分代码按C语言的方式进行编译。
由于c++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而c语言不支持函数重载,因此编译c语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
内联函数是把函数编译好的二进制指令直接复制到函数的调用位置。inline仅是对编译器的建议,如果函数太复杂(while),即使声明inline也不是内联函数。
优点:能够提高程序的运行速度,少去了函数调用时间
缺点:会导致可执行文件冗余,牺牲空间来换取时间
分类
new/delete是c++的关键字,需要头文件支持,而malloc/free是c语言的库函数,需要编译器支持,后者使用必须指明申请内存空间的大小,并且返回的是void指针需要强制转换,对于类类型的对象,后者不会调用构造函数和析构函数。new的底层分为两步,第一步先调用operator new申请空间,内部实现其实就是malloc,第二步调用对象的构造函数。
这是一个超链接
typeid
返回的是一个type_info
对象,对于c++的内置数据类型,typeid可以方便的输出它们的数据类型。
#include <iostream> #include <typeinfo> using namespace std; class A { public: void Print() { cout<<"This is class A."<<endl; } }; class B : public A { public: void Print() { cout<<"This is class B."<<endl; } }; int main() { int a = 1; A *pa = new B(); cout<<typeid(a).name()<<'\n';//int cout<<typeid(pa).name()<<'\n';//class A* cout<<typeid(*pa).name()<<'\n';//class A return 0; }
pa是一个A类型的指针,所以输出为指针类型,*pa表示pa指向的对象类型,所以输出class A。
但是pa明明指向的B,为什么得到的却是class A?
#include <iostream> #include <typeinfo> using namespace std; class A { public: virtual void Print() { cout<<"This is class A."<<endl; } }; class B : public A { public: void Print() { cout<<"This is class B."<<endl; } }; int main() { A *pA = new B(); cout<<typeid(pA).name()<<endl; // class A * cout<<typeid(*pA).name()<<endl; // class B return 0; }
使用type_info类中重载的==和!=比较两个对象的类型是否相等,通常用于比较两个带有虚函数的类的对象是否相等
#include <iostream> #include <typeinfo> using namespace std; class A{ public: virtual void Print() { cout<<"This is class A."<<endl; } }; class B : public A{ public: void Print() { cout<<"This is class B."<<endl; } }; void Handle(A *a){ if (typeid(*a) == typeid(A)) {cout<<"I am a A truly."<<endl;} else if (typeid(*a) == typeid(B)) {cout<<"I am a B truly."<<endl;} else {cout<<"I am alone."<<endl;} } int main(){ A *pA = new B(); Handle(pA); delete pA; return 0; } /* I am a B truly. */
dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。
当类中存在虚函数时,编译器就会在类的成员变量中添加一个指向虚函数表的vptr指针,每一个class所关联的type_info对象也经由虚函数表被指出来,通常这个type_info对象放在表格的第一个slot。当我们进行dynamic_cast时,编译器会帮我们进行语法检查。如果指针的静态类型和目标类型相同,那么就什么事情都不做;否则,首先对指针进行调整,使得它指向虚函数表,并将其和调整之后的指针、调整的偏移量、静态类型以及目标类型传递给内部函数。其中最后一个参数指明转换的是指针还是引用。两者唯一的区别是,如果转换失败,前者返回NULL,后者抛出bad_cast异常。对于在typeid函数的使用中所示例的程序,使用dynamic_cast进行更改,代码如下:
#include <iostream> #include <typeinfo> using namespace std; class A{ public: virtual void Print() { cout<<"This is class A."<<endl; } }; class B : public A{ public: void Print() { cout<<"This is class B."<<endl; } }; class C : public A{ public: void Print() { cout<<"This is class C."<<endl; } }; void Handle(A *a){ if (dynamic_cast<B*>(a)) {cout<<"I am a B truly."<<endl;} else if (dynamic_cast<C*>(a)) {cout<<"I am a C truly."<<endl;} else{ cout<<"I am alone."<<endl;} } int main(){ A *pA = new B(); Handle(pA); delete pA; pA = new C(); Handle(pA); return 0; } /* I am a B truly. I am a C truly. */
在一个类的虚函数表里面添加了一个新的条目。
每个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。函数参数的压栈顺序为从左往右。
生成一个临时变量,把它的引用作为函数参数传入函数内。
在c语言,NULL是个宏定义为void*(0)的东西
int *p = NULL;//这里发生了隐式转换,void*转化为int*类型
NULL在c++中是数字0,因为c++中void*类型是不允许隐式转化成其他类型
nullptr在c++中表示空指针
容器:一些封装数据结构的模板类。
算法:STL提供了非常多的数据结构算法,它们都被设计成一个个的模板函数。
迭代器:对容器中数据的读写,是通过迭代器完成的。
函数对象(仿函数):如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象。
本质:函数对象是一个类,不是一个函数
适配器:可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。
内存分配器:为容器类模板提供自定义的内存申请和释放功能,并不常用。
vector底层实现是数组,连续存储结构。每次push_back,会比较size和capacity,相等的话会新开辟一片两倍的capacity的空间,然后把元素迁移到新空间,释放掉原来的空间。
函数接口:
在底层。vector使用了三个指针:
size = last - first capacity = end - first
emplace_back()是在vector的内存中原地构造,而push_back()是在外部构造后,通过移动或者拷贝到vector中。所以emplace_back()效率会更优。
list底层实现是双向链表,非连续存储结构,每个元素维护一对前后向指针,所以支持正序和逆序遍历。
插入和删除高效,但随机访问效率低。每个元素需要维护额外的两个指针,空间开销较大。
list每次插入都会新开辟一个元素单位的空间存放。
list的优点:
list的缺点:
deque是双端队列,连续存储结构,它提供了两级数组结构,第一级完全类似vector,代表容器,第二级维护了一个容器的首地址。不仅拥有vector的所有功能,还支持高效的首部/尾部的添加和删除。可以理解为合并了vector和list的功能。deque容器的迭代器也是支持随机访问的。
deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据
中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间
map和set都是C++的关联式容器,其底层实现都是红黑树(RB-Tree)。由于map和set所开放的各种操作接口,RB-Tree也都提供了,所以几乎所有的map和set的操作行为,都只是转调RB-Tree的操作行为。
二者的区别:
二者都属于关联式容器,底层实现都是红黑树,所有元素都是pair,同时拥有键值(key)和实值(value)。pair的第一元素为键值,第二元素为实值。所有元素会根据元素的键值自动排序。
映射。map不允许键值重复。适用于有序键值对不重复映射。
多重映射。multimap允许键值重复。适用于有序键值对可重复映射。
基于红黑树实现,所有元素会根据元素的键值自动排序。不允许容器中有重复的元素。
基于哈希表,数据插入和查询时间复杂度几乎是常数时间,代价就是消耗较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash
函数对key
进行映射到不同区域进行保存。允许容器中有重复的元素。
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素而又无需暴露该对象的内部表示。
由于Iterator模式有以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如stl的list、vector、stack等容器类及ostream_iterator等扩展iterator。
迭代器不是指针,而是类模板。它只是通过重载指针的一些操作符(->、*、++、–等)模拟了一些指针的功能。迭代器封装了指针,是一个“可遍历stl容器内全部元素或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,可以根据不同类型的数据结构来实现不同的++、–等操作。
迭代器返回的是对象引用而不是对象的值,所以const只能输出迭代器使用*取值后的值而不能直接输出其自身。
iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
resize():改变当前容器内含有元素的数量(size()),size变为len,如果size<len,那么容器新增(len-size)个元素,默认为0,push_back(2)后,2放在下标为len上,size为len+1。
reserve():改变当前容器的最大容量(capacity()),不会生成元素,只是确定这个容器允许放入的对象大小,capacity变为len,如果capacity<len,那么会重新分配一个能存下len个对象的空间,然后把之前size个对象通过copy construtor复制过来,销毁之前的内存。
vector<int>a; a.reserve(100); a.resize(50); cout<<a.size()<<' '<<a.capacity()<<'\n';// 50 100 a.resize(150); cout<<a.size()<<' '<<a.capacity()<<'\n';//150 150 a.reserve(50); cout<<a.size()<<' '<<a.capacity()<<'\n';//150 150 a.resize(50); cout<<a.size()<<' '<<a.capacity()<<'\n';//50 150
这是一个超链接
STL的分配器用于封装STL容器在内存管理上的底层细节。
在C++中,内存配置和释放如下:
为了精密分工,STL中对象的构造由::construct()负责,对象析构由::destroy()负责;内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;此外,STL还提供一些全局函数,用来对大块内存数据进行操作。
同时为了提升内存管理效率,减少申请小内存造成的内存碎片问题,SGI STL采用两级分配器,当分配空间大小超过128bytes时,使用第一级空间配置器,直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放;当分配空间小于128bytes时,使用第二级空间配置器,采用内存池技术,通过空闲链表来管理内存。
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限。类内都能访问,类外只能访问public属性的成员,派生类可以访问基类protected属性的成员。
在C++中,可以用struct和class定义类,都可以继承。区别在于
struct的默认继承权限和默认访问权限是public,而class的默认继承权限和访问权限是private。struct可以继承class,同样class也可以继承struct,继承的默认权限取决于子类。
class可以定义模板类参数,而struct不用于定义模板参数
template <class T,int i>
如果没有定义构造函数,struct可以用大括号初始化,class需要所有成员变量都是public才可使用大括号初始化,如果定义了构造函数,二者均不可使用大括号进行初始化。
这是一个超链接
没有成员的结构体占1个字节,为了使得每个实例在内存中都有一个独一无二的地址。
偏移量:结构体变量中成员的地址和结构体变量地址的差。
存储变量时地址要求对齐,编译器在编译程序时会遵循两条原则
struct stru1 { int a; //start address is 0 char b; //start address is 4 int c; //start address is 8 }; struct stru2 { int i; //start address is 0 short m; //start address is 4 }; cout<<sizeof(stru2)<<'\n'//8
对于嵌套结构体或者结构体包含数组,需将其展开,原则变为
struct stru3 { char i; //0 struct{ char c;// 4 int j; //8 } tt; char a;//9 char b;//10 char d;//11 char e;//12 //或者char a[4] int f;//16 }; cout<<sizeof(stru3)<<'\n';//20
声明的类只是一种类型定义,类的大小指的是类的对象大小。
继承空类,空基类的1个字节不会加到派生类
一个类包含一个空类对象数据成员,其数据成员算1字节
class Empty {}; class HoldsAnInt { int x; Empty e; }; cout<<sizeof(HoldsAnInt)<<'\n';//8
含有虚函数的对象除了基本的数据类型,就是多了一个指向虚函数表的vptr(指针8个字节)。如果是继承,继承几个含有虚函数的类就多几个vptr。派生类中的虚函数地址会存放在第一个虚函数表中。
class A{ char ch; virtual void func0() { } }; class B{ char ch1; char ch2; virtual void func() { } virtual void func1() { } }; class C: public A, public B{ int c; virtual void func0() { } virtual void func1() { } }; cout<<sizeof(A)<<'\n';//16 = 8 + 8 cout<<sizeof(B)<<'\n';//16 cout<<sizeof(C)<<'\n';//32 = 8*2 + 8 + 8
虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。
class A { int a; virtual void myfunA(){} }; class B:virtual public A{ virtual void myfunB(){} }; class C:virtual public A{ virtual void myfunC(){} }; class D:public B,public C{ virtual void myfunD(){} }; cout<<sizeof(A)<<'\n';//16 = 8 +8 cout<<sizeof(B)<<'\n';//24 = 8 + 8 + 8 指向虚函数表指针+指向虚基类指针+int cout<<sizeof(C)<<'\n';//24 = 8 + 8 + 8 cout<<sizeof(D)<<'\n';//32 = 8 + 2*8 + 8 指向虚函数表指针+B中指向虚基类指针+C中指向虚基类指针+int
构造函数分为初始化和计算两个阶段,前者对应成员初始化链表,后者对应构造函数体。
class A{ public: A(int &target):a(target){ cout<<"构造函数"<<'\n'; } private: int a; }
类成员的初始化顺序不是按照初始化列表的顺序来的,而是按照类成员的声明顺序。
class A{ private: int i; int j; public: A(int _j,int _i):j(_j),i(_i){}//先初始化i,再初始化j,因为i的声明在j前面 }
构造函数先进行列表初始化,然后才会执行构造函数体。只有列表初始化才叫初始化,函数体内的赋值只能叫初赋值。
而基本的数据类型没有初始化都是默认值,以下三种必须通过初始化列表
浅拷贝:两个对象同时指向同一个内存,导致析构时,进行两次析构,同一块内存释放两次,会导致程序崩溃。
深拷贝:调用手写的拷贝构造函数,为新对象开辟一个新内存。
类中默认的拷贝构造函数都是浅拷贝。
比如两个函数只有返回值不同,主函数不需要接受这个返回值时,会产生二义性,编译器不知道要调用哪个重载的函数。
右值引用是C++11中引入的新特性,它实现了转移语义和精确传递。
有名称的、可以获取到存储地址的表达式即为左值;反之是右值。
int a = 5;//5是右值 int b = a;//a和b都是左值 5 = a;//错误,5不能作为左值 const int &c = 10;//常量左值引用操作右值,不可修改 int &&d = 10;//右值引用,必须初始化且只能使用右值进行初始化 d = 20;//右值引用可以修改 cout<<d<<'\n';//20
作用:
左值引用和右值引用的区别
引用类型 | 可以引用的值类型 | 使用场景 | |||
---|---|---|---|---|---|
非常量左值引用 | 常量左值引用 | 非常量右值引用 | 常量右值引用 | ||
非常量左值引用 | T | F | F | F | 无 |
常量左值引用 | T | T | T | T | 常用于类中构建拷贝构造函数 |
非常量右值引用 | F | F | T | F | 移动语义、完美转发 |
常量右值引用 | F | F | T | T | 无实际用途 |
这是一个超链接
c++11引入了移动语义和完美转发,两者都基于右值引用。
转移语义:将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。
无条件地将参数转为右值。
作用:减少资源开销和提高效率。
比如主函数接收一个函数返回一个类的实例对象,首先需要调用普通构造函数生成实例对象,因为对象是这个函数内部的,所以需要调用拷贝构造函数生成临时对象返回给主函数,主函数又会调用拷贝构造函数获得临时对象的值。进行了两次拷贝,消耗时间。而移动语义通过移动构造函数,将资源所有权给了临时对象,临时对象又转移给主函数部分。本质就是像交换(swap)。
template <typename T> class A{ private: int m_size; T* m_array; public: //移动构造函数 A(A&& rhs):m_size{rhs.m_size},m_array{rhs.m_array}{ rhs.m_size = 0; rhs.m_array = nullptr; } }
也可以使用std::move去实现
void swap(T& a, T& b){ T tmp{std::move(a)}; // 调用移动构造函数 a = std::move(b); // 调用移动赋值运算符 b = std::move(tmp); // 调用移动赋值运算符 }
不改变参数的左右值类型。
比如一个string的右值传递给wrapper,param为右值引用,一旦传入,param就变成左值,丢失了右值属性,使用std::forward确保传入foo的值还是一个右值。
// 目标函数 void foo(const string& str); // 接收左值 void foo(string&& str); // 接收右值 template <typename T> void wrapper(T&& param){ foo(std::forward<T>(param)); // 完美转发 }
这是一个超链接
主要分为4个过程
静态链接:在程序运行之前,将库函数全部载入到可执行的目标文件之中,这样子程序运行就脱离了库函数,优点是可移植性强,缺点也很明显,将不需要的库函数导入进去,消耗了太多内存。
动态链接:在程序运行之前,到需要调用的库函数的时候,去看其他执行的文件有没有,如果存在的话,共享这个库函数,直接调用,等于库函数只有一份内存拷贝,没有的话再进行动态链接,这样会耗费一些时间,并且可移植性差,但是与静态链接相比占用内存少,并且如果修改了库函数的话,静态链接需要重新编译,而动态链接则不需要,因为调用的是库函数的接口。
编译器预处理阶段查找头文件的路径不一样
在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。
在C++中,一般内存主要分为五部分:
代码区:存放程序代码,即CPU执行的机器代码,并且是只读的。
代码区的地址:函数地址、程序入口地址、程序的名字
函数名称是一个指针,可以通过查询函数名称所处的内存地址,查询函数存放的地址
void test(){ printf("main:0x%p\n",main);//打印main函数的存放地址 }
常量区:存放常量,不允许修改。
静态区(全局区):静态变量和全局变量的存储区域是一起的,一旦静态区的内存被分配,,静态区的内存直到程序全部结束之后才会被释放。
堆区:由程序员调用malloc()函数来主动申请的,需使用free()函数来释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。
栈区:存放函数内的局部变量,形参和函数返回值,由编译器自动分配和释放。
栈区之中的数据的作用范围过了之后,系统就会回收自动管理栈区的内存(分配内存 , 回收内存),。
内存泄漏:一般指的就是堆内存的泄露,因为堆的内容是程序员自己安排的。内存泄漏即new出来的东西没有delete,造成了内存被占用。
内存溢出:指所剩余的可申请内存空间小于要申请的空间,比如栈满时候进栈,就产生了内存溢出。
使用智能指针,自动删除分配的内存。
可以通过 # pragma pack(1) 取消内存对齐
这是一个超链接
段错误通常发生在访问非法内存地址的时候,存下以下几种情况: