面向对象程序设计(Object-Oriented Programming,OOP)是一种新的程序设计范型。程序设计范型是指设计程序的规范、模型和风格,它是一类程序设计语言的基础。
面向过程程序设计范型是使用较广泛的面向过程性语言,其主要特征是:程序由过程定义和过程调用组成(简单地说,过程就是程序执行某项操作的一段代码,函数就是最常用的过程)。
面向对象程序的基本元素是对象,面向对象程序的主要结构特点是:第一,程序一般由类的定义和类的使用两部分组成;第二,程序中的一切操作都是通过向对象发送消息来实现的,对象接收到消息后,启动有关方法完成相应的操作。a
(抽象)、封装、继承、多态。
类中内容包括数据(数据成员)和函数(成员函数)
按照访问权限划分:公有、保护、私有
class 类名{
public:
公有数据成员;
公有成员函数;
protected:
保护数据成员;
保护成员函数;
private:
私有数据成员;
私有成员函数;
};
#include
#include
using namespace std;
class CPeople{//类名一般大写C开头
public:
string m_strName;//类成员属性一般以 m_ + 数据类型 + 变量名
bool m_bSex;
int m_nAge;
void run() {
cout << m_strName << "在跑步" << endl;
}
};
int main()
{
CPeople peo;//定义一个对象(类的实例化),这时才会为CPeople这个类开辟空间
peo.m_strName = "小明";
cout << peo.m_strName << endl;
return 0;
}
访问修饰符(3种):描述了类中的成员的适用范围,即访问控制(访问权限)
class CPeople{//类名一般大写C开头
public:
string m_strName;//类成员属性一般以 m_ + 数据类型 + 变量名
protected:
bool m_bSex;
private:
int m_nAge;
public:
void run() {
cout << m_strName << "在跑步" << endl;
}
//---公共接口------
/*
公共接口存在的意义,可以在对类内私有数据做修改时,进行限制,避免随意修改
*/
int GetAge()
{
return m_nAge;
}
void SetAge(int num)
{
if (num >= 0 && num <= 200)
{
m_nAge = num;
}
}
};
int main()
{
CPeople peo;//定义一个对象(类的实例化),这时才会为CPeople这个类开辟空间
peo.m_strName = "小明";
cout << peo.m_strName << endl;
peo.SetAge(20);
cout << peo.GetAge() << endl;
return 0;
}
构造函数:
通常在定义变量时要初始化,在定义类对象时,成员属性并没有得到初始化。C++类提供了一种特殊的函数‐构造函数。
构造函数:其作用是用来初始化类成员属性。空类中存在一个默认的无参数的构造,函数名为当前类名,无返回值。
class CTest
{
//CTest(){ } //默认无参构造
};
构造函数并不需要我们手动调用,在定义对象的时候会自动调用,这个默认的无参构造是编译器给提供的,函数体代码为空,所以在定义对象时虽然调用了,但并没有这正给成员初始化。
所以需要手动重构构造函数。一个类中的构造函数允许存在多个,他们是函数重载的关系,重构的构造函数可以指定参数来符合我们需要的初始化过程。
注意:只要重构了任何的构造函数,编译器将不会再提供那个默认的无参构造了。
定义多个对象可能会执行不同的构造,这就要看在定义对象时如何指定参
数了,会根据参数的类
型、数量自动匹配对应的构造,但一个对象最终只能执行其中一个构造。
析构函数:与构造函数相对应的析构函数,其作用是用来回收在类中申请的额外的空间。空类中存在一个默认的析构函数,函数名为~类名,无返回值,无参数。
class CTest\
{
//~CTest(){ } //默认析构
};
析构函数在对象的声明周期结束的时候,自动调用,编译器提供的默认析构函数函数体代码也为空,我们可以手动重构,一旦重构,编译器就不会再提供那个默认析构了,与构造不同的是析构函
数只允许存在一个。
对象是类的实例化
空类占据1个字节,占位作用,标识当前对象真实存在内存空间中,区别于其他对象
有成员属性后,按照属性所占空间分配。
成员属性 --> 对象类不实例化成对象的话,就不存在成员属性
成员函数 --> 类
静态成员属性 --> 类
静态成员函数 --> 类
#include
using namespace std;
class CTest {
public:
int m_a;
CTest() {
}
void show() {
cout << "shuow" << endl;
}
void showA() {
cout << "shuow" << " "<<m_a << endl;
}
};
int main()
{
cout << sizeof(CTest) << endl;//空类 1个字节(占位标识符),有成员属性后,按照属性所占空间分配
CTest* p = nullptr;
//cout << p->m_a << endl;类不具体化成对象时,类中的成员属性并不存在,所以会报错
p->show() ;//类中的成员函数,存在,并且在 具体化对象后 也不占用对象的空间
//p->showA();//函数中用到了属性,所以也会出错
return 0;
}
#include
using namespace std;
class CTest {
public:
int m_a;
CTest() {
}
void show() {
cout << "show" << endl;
}
void showA(/*CTest * const this */) {
cout << "show" << " "<<m_a << endl;//等效下面一行,编译器自动添加了 this
//cout << "shuow" << " "<m_a << endl;
show();//等效于
//this->show();
}
};
int main()
{
cout << sizeof(CTest) << endl;//空类 1个字节(占位标识符),有成员属性后,按照属性所占空间分分配
CTest* p = nullptr;
//cout << p->m_a << endl;类不具体化成对象时,类中的成员属性并不存在
p->show() ;//类中的成员函数,存在,并且不占用对象的空间
//p->showA();//函数中用到了属性,所以也会出错
return 0;
}
类中的静态成员初始化,需要写在类外,格式 类型 类名:: 变量名 = 初始化值
注意:
#include
using namespace std;
/*
静态成员属性:属于类,,不包含在对象的内存空间中,编译期存在,与对象存在与否无关
*/
class CTest {
public:
static int m_a;//静态成员属性
int m_b;
CTest() {
m_a = 10;//赋值,不是初始化
m_b = 20;//初始化
cout << m_a << endl;
}
static void fun(){/*没有隐藏的指针this,就不能使用普通成员属性和成员函数*/
cout << "static void fun()" << endl;
}
};
/*
类中的静态成员初始化,需要写在类外,格式 类型 类名:: 变量名 = 初始化值 注意:去掉static
分文件编写的时候,写在.cpp中
*/
int CTest::m_a = 30;//初始化
/*
静态成员的使用,可以通过对象也可以不通过对象
*/
int main()
{
CTest tst1;
cout << tst1.m_a << endl;
cout<<CTest::m_a; //在没有对象的情况下,类名作用域直接使用
CTest tst2;
cout << &tst1.m_a << " " << &tst2.m_a << endl;
//通过对象去修改静态成员属性,对其他对象是可见的(使用的是修改之后的值)
tst1.m_a = 100;
cout << tst2.m_a << endl;
}
属于类的,在编译期存在,一个类只有一份,多个对象共享这一份函数
当类中有const类型的变量时,在定义的时候必须要初始化,而这个初始化操作是在 初始化参数列表 中完成的,而构造函数的函数体代码中进行的操作严格来说是赋值,先执行初始化列表,在执行构造函数体中的代码。对于普通的变量来说也可在初始化参数列表中初始化。
写法:在构造函数的参数列表后加上一个冒号 : 后面是初始化的成员,用圆括号 ()的形式指定初始化值,多个成员用 逗号,分割。
class CTest{
int m_a;
const int m_b; //常量
char m_c; //初始化参数列表
CTest(int b):m_a(10),m_b(b),m_c('c')
{
m_a = 20; //赋值,允许
//m_b = 30; //赋值,不允许
}
};
//我自己的例子
#include
using namespace std;
int main()
{
int m_a = 10;
const int m_b = 20;
//常量指针,通过指针 值不课改,可改指向
const int* p1 = &m_a;//或者 int congst * p1 =&m_a;
//*p1 = 30;非法操作
p1 = &m_b;
//指针常量, 通过指针 值可改,指向不可改
int* const p2 = &m_a;//初始化必须赋值
*p2 = 40;
//p2 = &m_b;非法操作
//常量指针常量,不能修改指向,!不能通过!指针改值
const int* const p3 =& m_a;
//*p3=100;
//p = &m_b;
m_a = 100;//合法
}
//科林的例子
int a=100;
int b =200;
const int *p1=&a; //等同于 int const *p2=&a;
//指明了 *p1 是不允许修改的,即 * p1=200; // 操作是非法的 ,p1 = &b; //是合法的
int * const p2=&a; //说明了 p2 的指向不能修改
//p2 =&b; //是非法的
//*p2 =300; //是合法的
const int c =100;
const int *const p3=&c; //指明了当前的指针指向不能修改,且指向的具体的空间的内容
const int*p3=&ma;//安全级别升级的操作
int* const p4 = &mb; // &mb = const int*, 指针的安全级别是降级操作,不允许
*p4=60;
#include
using namespace std;
class CTest {
public:
int m_a;
const int m_b;
static int m_c;
CTest():m_a(10), m_b(20) {
}
void show(/*CTest const this */) {
this->m_a = 40;
fun();
//或者this->fun();
/*相当于
CTest tst;
CTest* const pthis2 =& tst;
const CTest* const pthisl = pthis2; // 指针的安全级别升级操作,允许的
*/
}
/*
区别:this指针类型不同,对于常函数来说有双重const修饰,在常函数中不能修改类中的非静态成员属性,
常含数不能调用普通函数,但是可以调用静态函数
*/
/*常函数,在参数列表后面加const, const修饰了* this, 意味着this指针指向的对象里的成员属性不能修改*/
/*const */void fun(/*const CTest*const this *//*const int a*/)const {
cout << m_a << " " << m_b << endl;
// ma = 30;//即使是变量也不能在常函数中修改
//mb=40:
//this->m_a=20;不可修改
this->m_a;
this->m_b;
m_c = 50;//可以修改静态成员,因为静态成员可以不用this调用
//this->show();
//show();
/*相当于
CTest tst;
const CTest*const pthisl =&tst;
CTest*const pthis2 pthis1://指针的安全级别降级操作,不允许
*/
}
};
int CTest::m_c = 30;
int main()
{
//int m_a = 10;
//const int m_b = 20;
//const int*p3=&ma;//安全级别升级的操作
// int* const p4 = &mb; // &mb = const int*, 指针的安全级别是降级操作,不允许
//*p4=60;
return 0;
}
内联函数C++为了提高程序的运行速度所做的一项改进,普通函数和内联函数主要区别不在于编写 方式,而在于C++编译器如何将他们组合到程序中的。编译器将使用相应的函数代码替换到内联函数的调用处,所以程序无需跳转到另一个位置执行函数体代码,所以会比普通的函数稍快,代价是 需要占用更多的内存,空间换时间的做法。
执行函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中, 然后才能执行函数体中的代码,代码执行完毕后还要将之前压入栈中的数据都出栈。这个过程中涉及到空间和时间的开销问题,如果函数体的中代码比较多,逻辑也比较复杂,那么执行函数体占用 大部分时间,而函数调用、释放空间过程花费的时间占比很小可以忽略;如果函数体的中代码非常少,逻辑也非常简单,那么相比于函数体代码的执行时间 函数调用机制所花费的时间就不能忽略 了。
int add(int a,int b)
{
return a+b;
}
int c = add(1,2);
所以为了消除函数调用的时间开销,C++提供一种提高效率的方法 inline函数,上例中的add函数 可以变为内联函数,如下 ,内联函数在编译时将函数调用处用函数体替换(类似于宏)。
inline int add(int a,int b)
{
return a+b;
}
int c = add(1,2); //替换后:int c = 1+2;
class CTest
{
public: int show(){ //默认内联
int a = 1;
return a;
}
};
int main()
{
CTest tst;
tst.show(); //内联替换
return 0 ;
}
是一种"is a part of"的关系,部分与整体,包含与被包含。组合是一个类中包含另一个类对象。相比聚合,组合是一种强所属关系,组合关系的两个对象往往具有相同的生命周期,被组合的对象是在组合对象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对象独立存在,整体不存在,部分一定不存在。
举例:人与手、人与头之间的关系,人需要包含头和手,头、手是人的一部分且不能脱离人独立而存在。 在C+语法中,通常在组合类中包含被组合类对象来实现组合关系:
class CHand{};
class CPeople{
CHand m hand;//组合(复合)关系
}
是一种"use a"的关系。一个对象的某种行为依赖于另一个类对象,被依赖的对象视为完成某个功能的工具,并不持有对他的引用,只有在完成某个功能的时候才会用到,而且是必不可少的。依赖之间是没有生命周期约束关系的。
举例:人要完成编程这件事,那么需要用到电脑,电脑作为一个工具,其他的时候不需要,电脑也不可能作为人的属性而存在(非组合关系),人必须依赖于电脑才能完成编程这件事。 C++语法中,代码的表现形式为多种,通常将被依赖的对象作为另一类方法的参数的形式实现两个类之间的依赖关系。
class CComputer{};
class CPeople{
void Code(CComputer *pc)//或:CComputer &pc,
};
是一种"has a"的关系。关联不是从属关系,而是平等关系,可以拥有对方,但不可占有对方。完成某个功能与被关联的对象有关,但是可有可无。被关联的对象与关联的对象无生命周期约束关系,被关联对象的生命周期由谁创建就由谁来维护。只要二者同意,可以随时解除关系或是进行关联,被关联的对象还可以再被别的对象关联,所以关联是可以共享的。
举例:人和朋友的关系,人要完成玩游戏这个功能,没有朋友可以自己玩游戏,如果交到朋友了就可以和朋友一起玩游戏。 C++语法中,通常在关联的类中定义被关联类对象的指针形式实现两个类之间的关联关系。
class CFriend{};
class CPeople{
CFriend*m pFriend;//关联关系
}
它是一种"owns a"的关系。多个被聚合的对象聚集起来形成一个大的整体,聚合的目的是为了统一进行管理同类型的对象,聚合是一种弱所属关系,被聚合的对象还可以再被别的对象关联,所以被聚合对象是可以共享的。虽然是共享的,聚合代表的是一种更亲密的关系,相当于强版本的关联。
举例:一堆人组成一个家庭,进行统一管理完成敲代码工作。 C++语法中,通常在聚合类中定义被聚合对象指针的数组、链表等容器。
class CPeople{};
class CFamily{
CPeople*m_pFamily [10];
}
#include
using namespace std;
class CHand {
public:
void move() {
cout << "我的手在移动" << endl;
}
};
class CComputer {
public:
void compile() {
cout << "电脑正在编译代码 生成可执行程序" << endl;
}
};
class CFriend {
public:
void play() {
cout << "我的朋友正在玩耍" << endl;
}
};
class CPeople {
public:
CHand m_hand;
CFriend* m_pFri;
public:
void clap() {
m_hand.move();
cout << "手掌撞击 发出掌声" << endl;
}
void code(CComputer* pCom) {
if (pCom) {
m_hand.move();
cout << "敲出一行行代码...." << endl;
pCom->compile();
}
else {
cout << "我无法完成敲代码的功能" << endl;
}
}
void palyWangZhe() {
if (m_pFri) {//有朋友
cout << "找朋友组队.." << endl;
m_pFri->play();
cout << "我和我的朋友一起打王者,victory" << endl;
}
else {//没朋友
cout << "我和自己打王者,victory" << endl;
}
}
};
class CFamily {
public:
CPeople* m_arrPro[10];
CFamily() {
for (CPeople*& peo : m_arrPro) {
peo = nullptr;
}
m_arrPro[0] = new CPeople;
m_arrPro[1] = new CPeople;
m_arrPro[2] = new CPeople;
}
~CFamily() {
for (CPeople*& peo : m_arrPro) {
if (peo) {
delete peo;
peo = nullptr;
}
}
}
public:
void AllPeopleCode(CComputer* pCom) {
for (CPeople* peo : m_arrPro) {
if (peo) {
peo->code(pCom);
}
}
}
};
int main()
{
{
//组合
CPeople peo;
peo.clap();
cout << endl;
}
{
//依赖
CComputer* pCom = new CComputer;
CPeople peo;
peo.code(pCom);
delete pCom;//回收电脑
pCom = nullptr;
peo.code(pCom);//此时没有电脑
}
{
//关联
CPeople peo;
peo.palyWangZhe();
CFriend* pFri = new CFriend;
peo.m_pFri = pFri;//结交朋友
peo.palyWangZhe();
peo.m_pFri = nullptr;//和朋友绝交
peo.palyWangZhe();
delete pFri;
pFri = nullptr;
}
{
//聚合
CComputer comp;
CFamily fam;
fam.AllPeopleCode(&comp);
}
return 0;
}
#include
using namespace std;
class CStepFather {
public:
int m_a;
CStepFather() {
m_a = 10;
}
CStepFather(int a) {
m_a = a;
}
};
class CSon
{
public:
CStepFather m_stepFa;//包含另一个类的对象
int m_son;
public:
CSon() /*:m_stepFa() */
{ // 当包含另一个类的对象,编译器会自动调用无参的构造进行初始化
m_son = 20;
}
CSon(int a) :m_stepFa(a)
{//如果想调用带参数的构造函数进行初始化,需要手动显式指定
m_son = 20;
}
CSon(int a, int b) :m_stepFa(a) {
// 如果另一个类中没有无参的构造函数,必须显式的指定带参数的构造函数,如果什么也不指定,会报错
m_son = 20;
}
};
int main()
{
CSon son;
cout << son.m_son <<" "<<son.m_stepFa.m_a << endl;
CSon son2(100);//匹配的是CSon和CStepFather的带参数的构造函数
cout << son2.m_son <<" "<<son2.m_stepFa.m_a << endl;
return 0;
}
被继承的类叫做基类(父类),继承的类叫派生类(子类),在派生类类名后面加 : 继承方式 ,基类子类包含且可以使用父类的成员.
class CFather{};
class CSon:public CFather{};
#include
using namespace std;
//继承:子类包含且可以使用父类的成员
class CFather {
public:
int m_a;
int m_money;
CFather()
{
m_money = 100;
}
};
class CSon :public CFather {
public:
int m_b;
int m_money;
CSon() {
m_a = 10;
m_b = 20;//子类可以直接使用父类的成员
m_money = 200;
}
void show() {
cout << m_a << " " << m_b << endl;
cout << CSon::m_money << endl;//当子类成员和父类成员同名时,在成员前面加上作用域得以区分,默认是子类
cout << CFather::m_money << endl;
}
};
int main()
{
CSon p1;
p1.show();
cout << endl;
cout << p1.m_money << endl;
cout << p1.CSon::m_money << endl;
cout << p1.CFather::m_money << endl;//显示指定
return 0;
}
子类继承父类,相当于将父类的成员包含到自己的类里,所以定义子类对象所占用的空间大小除了 子类自身的成员还包括父类的成员。
成员在内存空间分布为:
我们可以将类中的一些功能相近、相似的共同的方法,抽离出来放到单独的一个类 中,并让其继承这个类,那么抽离出来的类就是父类,将来其他类在增加公共的方法时,我只需要 在父类添加一份即可。
提高了代码的复用性、扩展性。
当子类成员和父类成员同名时,默认是子类,在成员前面加上作用域得以区分。
子类中出现和父类中同名的函数,会隐藏父类中的函数。这种关系叫隐藏。若想使用父类中的函数,可以加上类名作用域。
public 、protected、private, 和 访问修饰符共同决定了父类中的成员在子类中的使用范围,所表现属性 及 访问控制
默认继承方式:private 私有继承
继承方式 | 父类中的属性 | 子类中的属性 |
---|---|---|
public | public | public |
protected | protected | |
private | 不可访问 | |
protected | public | protected |
protected | protected | |
private | 不可访问 | |
private | public | private |
protected | private | |
private | 不可访问 |
真父类->假父类->子类
在子类创建对象的时候,执行子类的构造函数(注意这里并不是直接先执行父类的 构造函数),但要先执行子类的构造的初始化列表,在初始化列表中会默认调用父类的无参构造初 始化父类成员,如果父类只有带参数的构造,那么需要在子类的初始化参数列表显示的指定父类的 初始化。这有点像之前说的组合关系形式。
和构造的顺序相反,先析构在回收对象
析构子类->回收子类---->析构假父类->回收假父类---->析构父类->回收父类
子类对象的生命周期结束后,因为是子类所以自动调用子类析构,当析构执行完 了,才会回收对象分配的空间,当然这个空间包含创建的父类的成员,那么回收父类成员前,自动 调用父类的析构。如果是new出来的子类对象,同理。
对于函数重载而言,我们调用的时候,可以根据参数类型、参数个数,编译器自动区分该具体调用 哪个函数。同样如果在一个类中存在两个同名函数(参数列表不同),那么也可以根据调用者传递 的参数自动的区分执行哪个函数,因为也是一个函数重载的关系。
那对于父类和子类中,如果有同名的函数但是参数列表不同,则不能够自动区分,因为他们之间的 关系并不是函数重载的关系,作用域不同,必须使用 类名:: 去区分到底该调用哪个函数。子类中和父类同名的函数,称之为 隐藏 。
在继承关系下,允许父类的指针指向子类的对象,但是反过来却不行。
#include
using namespace std;
void show(int a) {
cout <<"show(int a)" << a << endl;
}
void show(char a) {
cout << "show(char a)" << a << endl;
}
class A {
public:
void show(/* A * const this */int a, int b) { //和子类中同名的成员的关系可以称之为:隐藏
cout << "A::show(int a, int b) " << a << " " << b << endl;
}
};
class AA:public A {
public:
void show(int a) {
cout << "AA::show(int a)" << a << endl;
}
//也是函数重载
void show(char a) {
cout << "AA::show(char a)" << a << endl;
}
};
int main() {
show(10); //可以根据实参自动匹配对应的重载函数
show('a');
/*AA aa;
aa.show(10);
aa.show('b');*/
//aa.show(10,20); //想去匹配父类的函数,但无法自动匹配,不是函数重载,因为不在一个作用域下
//-----------------------------------
//AA aa;
//aa.show(10, 20); //子类的同名的函数注释掉,可以匹配父类的函数了
AA aa;
aa.A::show(10,20); //显式指定父类的show函数
A* pthis = &aa; //继承下,父类的指针不通过强转可以直接指向子类对象
return 0;
}
首先,使用函数指针的优点–>通过函数指针调用
void (CTest::*p_fun)() = &CTest::show; //定义类成员函数指针并初始化,注 意: & 必须加上,
typedef void (CTest::*P_FUN)(); //使用typedef 进行优化
P_FUN p_fun2 = &CTest::show;
(tst.*p_fun2)(); //普通对象通过指针调用类成员函数
(pTst ‐>* p_fun2)(); //指针对象通过指针调用类成员函数
#include
using namespace std;
void show() {
cout << "show" << endl;
}
class CTest {
public:
void show(/* CTest const this */) {
cout << "CTest::show" << endl;
cout << "this" << endl;
}
};
/*
区别:
1.作用域不同。
2.非静态类成员函数有隐藏的this指针,全局的函数没有
*/
int main() {
CTest tst;
tst.show();
//void (*p_fun)() = &tst.show;//这是一个错误的写法
//void (*p_fun)() = &CTest::show;//无法从“void(CTest ::*)(void)” 转换为 “void(*)(void)”
// 类成员函数指针,C ++ 中提供的一个整体操作 ::* ,用来定义类成员函数指针
void (CTest :: * p_fun)() = &CTest::show;
(tst .* p_fun)();///.*、->* c++中提供的一个整体操作,通过对象调用类成员函数指针指向的类成员函数
CTest* pTst = new CTest;
(pTst->*p_fun)();
cout << pTst << endl;
return 0;
}
相同的行为方式可能导致不同的行为结果,即产生了多种形态行为,即多态,同一行语句展 现了多种不同的表现形态
多态的本质:
定义父类的指针可以指向任何继承于该类的子类的对象,且父类的指针具有子类对象的行为,多种子类表现为多种形态由父类的指针进行统一,那么这个父类 额指针就具有了多种形态
虚函数列表是属于类的,而不是属于某个对象的,但是__vfptr 是属于对象。每个类都会有对应的 虚函数列表(前提是有虚函数),由于子类继承父类,那么也会继承父类的虚函数列表,编译器会 检查子类是否有重写父类的虚函数,如果有,那么在子类的虚函数列表中会替换掉父类的虚函数地 址,我们称之为覆盖,指向了子类中的虚函数。如果子类没有重写的父类虚函数,依然会保留在子 类的虚函数列表中,如果子类有自己的虚函数,那么会顺序添加到虚函数列表中,这个过程在编译 阶段就完成了。 多态的前提条件是父类的指针指向子类对象,那么虚函数指针在子类中被初始化指向子类的虚函数 列表,也就是说new的哪个子类对象,_ vfptr就指向了哪个子类的虚函数列表。
继承多态下,子类的虚函数列表:
class CFather
{
public:
virtual void show()
{ //虚函数
cout<<"CFather::show"<<endl;
}
};
class CSon:public CFather
{ //继承
public:
virtual void show()
{
//重写了父类的虚函数,即使不加 virtual 也会认为是虚函数
cout<<"CSon::show"<<endl;
}
CFather *pFa = new CSon; //父类的指针指向子类对象
pFa‐>show(); //调用的是子类的函数
#include
using namespace std;
class CFather {
public:
virtual void fun1() {
cout << "CFather::fun1()" << endl;
}
virtual void fun2() {
cout << "CFather::fun2()" << endl;
}
};
class CSon :public CFather{
public:
virtual void fun1() {
cout << "CSon::fun1()" << endl;
}
virtual void fun3() {
cout << "CSon::fun3()" << endl;
}
};
class CSon2 :public CFather {
public:
virtual void fun1() {
cout << "CSon2::fun1()" << endl;
}
};
/*继承多态下
*虚函数指针指向哪个类的虚函数列表,取决于定义哪个类的对象(new哪个子类)
*/
int main() {
CFather* pFa1 = new CFather;//虚函数指针指向 父类CFather 的虚函数列表
CFather* pFa2 = new CSon;//虚函数指针指向 子类CSon 的虚函数列表
CFather* pFa3 = new CSon2;//虚函数指针指向 子类CSon2 的虚函数列表
CSon son;
/*
[0]:CSon::fun1
[1]:CFather:fun2
*/
pFa2->fun1();
pFa2->fun2();
((CSon*)pFa2)->fun3();
return 0;
}
在多态下,父类的指针指向子类的对象,最后在回收空间的时候,却是按照父类的指针类型delete 的,所以只调用了父类的析构,子类的析构并没有执行,这样的话就有可能导致内存泄漏。
用虚析构来解决,即把父类的析构函数变为虚析构函数,这样的话,再去按照父类的指针 delete时,调用析构会发生多态行为,从而真正调用的是子类的析构,最后回收对象内存空间时, 再调用父类的析构。
class CFather
{
virtual ~CFather()
{ //虚析构
}
};
class CSon:public CFather
{
virtual ~CSon()
{//虚析构
}
};
使用多态时,父类的析构,一定是虚析构;子类加不加virtual都可。
在多态下,有时抽象出来的父类的虚函数,并不知道如何实现,只有继承的子类才可能知道具体的 实现,可以把父类的虚函数变为纯虚函数。
#include
using namespace std;
class CFather//包含纯虚函数的类,不能实例化,又叫 抽象类
{
public:
virtual void fun() = 0;//纯虚函数,子类必须重写。只在父类声明,不需实现
virtual void fun1()
{
cout << "Father::fun()2" << endl;
}
};
class CSon : public CFather//抽象类的派生类,又称 具体类
{
public:
virtual void fun()
{
cout << "CSon::fun()" << endl;
}
virtual void fun1()
{
cout << "Son::Pfun()1" << endl;
}
};
int main()
{
CFather* pFa = new CSon;
pFa->fun();
//CFather fa;//包含纯虚函数的类不能实例化,叫抽象类
return 0;
}
#include
using namespace std;
class CFather {
public:
virtual void fun() {
cout << "CFather::fun" << endl;
}
};
char arr[20] = { 0 };
class CSon:public CFather {
private: //编译期的限制,多态是运行期多态,所以限制不住,能调用
virtual void fun() {
cout << "CSon::fun" << endl;
}
private:
void fun2() {
cout << "CSon::fun2" << endl;
}
public:
static void GetFun2() {
printf("%p %d\n",&CSon::fun2, &CSon::fun2);
sprintf_s(arr,"%d", &CSon::fun2);
cout << arr << endl;
}
};
union AA {
int a;
void (*p_fun)();
}aa;
int main() {
/*const bool a = true; //优化的例子
if (a) {
cout << "aa" << endl;
}*/
//
// //编译期确定
//#if __cplusplus
//#define A 10
//#else
//#define A 20
//#endif //__cplusplus
//
// int a = A;
// cout << a << endl;
//
//
// //运行期确定
// int b = 0;
// cin >> b;
// if (b == 10) {
// cout << "bbb" << endl;
// }
// else {
// cout << "aaa" << endl;
// }
//
// //-------------------------------------
// //编译期错误
// //int len = 10; //error C2131: 表达式的计算结果不是常数
//int arr[len] = { 0 };
// //const int len = 9999999999999;
// //int arr[len] = { 0 }; //error C2148: 数组的总大小不得超过 0x7fffffff 字节
//
// //运行期错误
// //int l = 9999999999999;
// int l = 9;
// int * p = new int[l];
// //p[20] = 0; //数组越界,运行期错误
//
// cout << "//---------------------------" << endl;
// CFather* pFa = new CSon;
// pFa->fun(); //CSon::fun
//----------------------------
CSon::GetFun2();
aa.a = atoi(arr); //将十进制字符串转成数字
cout << aa.a << endl;
(*aa.p_fun)();
return 0;
}
头文件(.h文件):将声明的变量、类型、函数、宏、类的定义
源文件:将变量初始化、函数的实现放于源文件中
这样方便于我们去管理、规划,更重要的是避免了重定义的问题。
两者的区别:
默认情况下 ,头文件不参与编译,而每个源文件自上而下独立编译。
静态常量成员一定要在源文件中进行初始化。
常函数数在头文件中声明,在源文件中实现除了要加类名:: ,要保留const关键字。
静态函数在头文件中声明,在源文件中实现除了要加类名:: ,要去掉static 关键字。
虚函数数在头文件中声明,在源文件中实现除了要加类名:: ,要去掉virtual关键字。
纯虚函数不需要实现
如下图头文件,BB.h和CC.h都包含的AA.h,main函数有都包含了BB.h和CC.h,就会出现AA.重复包含的问题
解决方案1:
在AA.h第一行加一句#pragma once
# pragma once: 直接和编译器沟通,告诉编译器当前的头文件在其他源文件中,只包含一次, 直接和编译器沟通 效率高,代码编译的速度快,不需要考虑宏重名的问题
解决方案2:
基于宏的逻辑判断,编译的速度慢,有一定的概率,宏的名字会重复,导致程序错误。
#ifndef __宏__ //判断当前宏是否定义,如果没定义的话,就定义宏#define
#define __宏__
...我的代码...
#endif //__宏
宏起到替换作用,一般写法 :
#define N 10
一个标识符被宏定义后,在用到宏 N的地方替换为10,宏替换发生在编译前。
宏是可以传参数的,在宏名字后面加 ﴾PARAM﴿ ,参数的作用也是一个替换。
#define N(PARAM) int a = PARAM;
一般情况下,宏替换当前这一行的内容,但是如果要替换多行内容,需要用到 \ 这个字符,用来连接当前行和下一行,一般最后一行不加\,\ 后面不能有任何字符,包括空格、tab、注释等。
#define RANGE()\
for(int i=0;i<NUM;i++){\
cout<<i<<" ";\
}
使用宏替换需要注意,宏及参数并不会像函数参数一样自动计算,也不做表达式求解。宏替换就是文字的替换。
#define N 2+3
int a = N*2; //2+3*2 = 8 不是 10
#define N(A,B) A*B
int c = N(1+2,3); //1+2*3 = 7 并不是9
可以通过使用()控制运算先后顺序
#define N (2+3)
int a = N*2; //(2+3)*2 = 10
#define N(A,B) (A)*(B)
int c = N(1+2,3); //(1+2)*3 = 9
#define HH(A) #A
#define JJ(A) #@A
#define KK() int a##b=10
C++ 提供的运算符,通常只支持对于基本数据类型和标准库中提供的类进行操作,对于自定义类型如果想通过操作符实现对应的操作,需要自定义重载的操作符并实现具体的功能。
重载操作符(也称为重载运算符):本质上是一个比较特殊的函数,函数名operator后接要重载的操作符, 参数要根据操作符的使用规则来,与使用时的类型、顺序、数量要保持一致。 一般要有返回类型,为了和后续的操作符继续去操作。 告诉编译器当遇到这个操作符时,应当调用该函数来实现一个操作符的功能,是对原来的操作的功能的扩展
在类内重载,作为类成员函数,需要用对象调用,使用场景需要根据函数的参数一致(包括类型和顺序),注意在类内重载的操作符函数有隐藏的this指针作为第一个参数。在使用是要注意重载操作符的参数类型和顺序,可以直接使用操作符,也可以显示通过对象调用重载的操作符
class CTest{
private:
int m_a;
public:
CTest()
{
m_a = 10;
}
int operator+(int a)
{
return m_a+a;
}
};
int main()
{
CTest tst;
int a = tst+20;
int b = tst.operator+(20);
//int c = 20+tst; //非法
return 0;
}
对于单目运算符++,有左++ 和 右++ 两种,为了区分右++,我们需要额外指定一个int类型的参数,这个参数只是用来区分,并无实际意义。
int operator++(); //左++
int operator++(int a); //右++
类外:作为全局的重载操作符函数,没有隐藏的ths指针参数,会比类内的重载操作符函数多一个参数,且参数的顺序不是固定的,可以进行调整, 注意是否与类内的重载操作符函数产生歧义。
class CTest{
public:
int m_a;
CTest()
{
m_a = 10;
}
int operator+(int a)
{
return this->m_a+a;
}
};
int operator+(int a,CTest &tst)
{
return tst.m_a+a;
}
int main()
{
CTest tst;
tst+10; //匹配类内重载函数
10+tst; //匹配类外重载函数
return 0;
}
class CTest
{
public:
int m_a;
CTest()
{
m_a = 10;
}
int operator+(int a)
{
return this->m_a+a;
}
};
int operator+(CTest &tst,int a)
{
return tst.m_a+a;
}
int main()
{
CTest tst;
tst+10; //error C2593: “operator +”不明确
return 0;
}
上面重载等号操作符 operator=,能让其他的类型赋值到当前类对象中,但是如果反过来写则会报错,类型不匹配,因为operator=只能在类内重载。此时可以重载某个类型,这样定义该类对象就可以像这个类型一样去使用。
函数格式为:
operator type()
{
return type_value; // 类型要和type 一致。
}
函数在写法上无参数,无返回值,但函数体中应该有return,且return 的变量类型要和重载的类型一致。例:
operator int()
{
int a=10;
return a;
}
下面两种都是错误写法:
int operator int() //error C2549: 用户定义的转换不能指定返回类型
operator int(int a) //error C2835: 用户定义的转换“CTest::operator int”不接受形参
int a = tst+10; //operator+
a = 10+tst; //operator int
a = tst; //operator int
当然也可以显示的调用 类型转换函数
int a = tst.operator int()+10; //operator int
一个类中构造函数可以重载,允许多个存在。
如果重载的构造函数只有一个参数且非当前类对象时,可以称之为转换构造函数,其可以将其他类型转自动换为当前类类型,这个过程为隐式类型转换。
class CTest
{
public:
int m_a;
CTest(int a)
{
m_a = a;
}
};
CTest tst(10); //调用带参数的构造
CTest tst2 = 20; //合法操作 将int类型转换为CTest类型
tst2 = 30; //合法操作 发生隐式类型转换
注意:如果是多个参数且无默认值时,则不能自动隐式类型转换。如果想要避免隐式类型转换,在构造函数前加上 关键字:explicit。
拷贝构造函数是众多构造函数中的一种,参数比较特殊是当前类对象的引用,它也是编译器默认给提供的,在空类中它与默认的无参构造并存,当我们手动重构拷贝构造函数时,编译器就不会提供默认的拷贝构造了,当然也不会存在默认的无参构造了。
当用一个类对象给类的另一个对象初始化时,会调用拷贝构造函数
class CTest
{
CTest();
CTest(const CTest &tst);
};
CTest tst1; //调用无参构造
CTest tst2(tst1); //调用拷贝构造
默认拷贝构造函数与默认无参构造不同,其函数体代码不为空,操作为 参数中对象成员依次给this对象成员进行初始化。
默认拷贝构造函数是一个浅拷贝,当类中存在指针成员且指向了一个具体的空间,拷贝构造函数只是将两个指针里存储的地址进行一个值传递,并不会处理指针指向的空间。这样就导致了多个对象里的指针指向了同一个空间,那么会导致以下两个问题:
解决办法: 深拷贝,它并不是一个固定的写法,而是一个解决的办法:即在拷贝构造时,如果参数对象中的指针成员指向了一个内存空间,那么在重构拷贝构造时,需要为当前this对象中指针成员额外开辟新的内存空间,并初始化对应的值。
在某些情况下,可以使用指针或引用可以避免对象的值传递,也避免了浅拷贝问题。
空类中编译器也会默认提供一个operator=函数,参数和 返回值 为当前类对象的引用,如果我们手动重构了编译器也就没必要为我们提供了。
当用一个类对象给类的另一个对象赋值时,会调用默认的operator=函数。
class CTest
{
CTest & operator =(const CTest &tst);
};
CTest tst1;
CTest tst2;
tst2 = tst1; //operator =
默认的operator=函数的函数体代码不为空,参数中对象成员依次给this对象成员进行赋值。他同拷贝构造函数一样,编译器默认生成的也是一个浅拷贝,解决方法深拷贝。
空类中存在的默认的函数4个:
默认无参数构造
默认的拷贝构造
默认的operator=
默认析构函数
STL (Standard Template Library),即标准模板库,是一个高效的C++程序库。
STL是 ANSI/ISOC++标准函数库的一个子集,它提供了大量可扩展的类模板,包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。对于其中的数据结构,用户可能需要反复的编写一些类似的的代码,只是为了适应不同数据的类型变化而在细节上有所出入。如果能够将这些经典的数据结构,采用类型参数的形式,设计为通用的类模板和函数模板的形式,允许用户重复利用已有的数据结构构造自己特定类型下的、符合实际需要的数据结构无疑将简化程序开发,提高软件的开发效率,这就是STL编程的基本设计思想。
C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector,string,list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在STL使用过程中,并不会感到陌生。
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也称为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。
分为两大类
1、序列性容器
元素保特了在容器中的原始的位置,允许指定元素的插入位置,每个元素都有固定的位置,位置取决于插入的时间和地点,06list 、 01vector 、 02deque
2、 关联性容器
元素的位置取决于容器的特定排序规则,和元素的值有关, 04map、03set、hash-map
在STL程序设计中, 容器 ( container)就是通用的数据结构。容器用来承载不同类型的数据对象,就如同现实生活中,人们使用容器用来装载各种物品样,但C++中的容器述存在一定的“数据加工能力”,它如同一个对数据对象进行加工的模具,可以把不同类型的数据放到这个模具中进行加工处理,形成具有一定共同特性的数据结构。例如将int型、char型或者 float型放到队列容器中,就分别生成int队列、char型队列或者foat型队列,它们都是队列,具有队列的基本特性,但是具体数据类型是不一样的STL容器主要包括向量( vector)、列表(list)、队列(deque)、集合( set)和映射( map)等。STL用模板实现了这些最常用的数据结构,并以算法的形式提供了对这些容器类的基本操作STL中的所有容器都是类模板,是一个已经建立完成的抽象的数据结构,因此可以使用这些容器来存储任何类型的数据,甚至是自己定义的类,而无需自己再定义数据结构。例如利用 deque容器,就很容易建立一个队列。
swap(arr[1],arr[2]);//交换
reverse(arr.begin(),arr.end());//反转
sort(arr.begin(),arr.end());//排序,并返回一个新的数组end的指针
int newlenght = unique(arr.begin(),arr.end())-arr.begin();//unique去重
for(int i=0;i排好序的数组
int n = lower_bound(arr.begin(),arr.begin()+length,8)-arr.begin();//查找8返回下标 (lower_bound是返回第一次出现的指针位置) (upper_bound是返回第一次出现的指针位置 )
vector是一段连续的内存地址,基于数组实现,管理器可以动态改变对象长度,在 vector:容器中有以下几个关于大小的函数:
size():返回容器的大小
max_size():返回容器扩展极限的最大存储的元素数量
empty():判断容器是否为空
capacity():返回容器当前能够容纳的元素数量
//1.初始化
vocter vec1;
vocter vec2(10);//10个值为0的元素
vocter vec3(10,4);//10个值为4的元素
//2.常用操作方法
vec1.push_back(100); //尾部添加元素
int size = vec1.size(); //元素个数
bool isEmpty = vec1.empty(); //判断是否为空
cout<=、<=
vector :: iterator iter = vec1.begin(); //获取迭代器首地址
vec1.clear(); //清空元素,其他元素前移
//3.遍历
//下标法
int length = vec1.size();
for(int i=0; i :: iterator iter = vec1.begin();
for( ; iter != vec1.end(); iter++)
{
cout<<*iter;
}
//vectorv1;
//vectorv2(10);
//vectorv3(10,1);
vectorv(10, 1);
for (int i = 0; i < v.size(); i++) {
v[i] = i+1;
}
cout << endl;
v.pop_back();
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
v.erase(v.begin(), v.begin() + 2);
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
vectorv2;
for (int i = 0; i < v2.size(); i++) {
v2[i] = i + 1;
}
vectorv3;
for (int i = 0; i < v3.size(); i++) {
v3[i] = i + 1;
}
cout << endl;
if (v3 == v2) {
cout << "相同" << endl;
}
else {
cout << "不相同" << endl;
}
v.clear();
cout << v.size();
// 案例
#include
#include
using namespace std;
int main()
{
vector vec1;
for(int i=0;i<10;i++)
{
vec1.push_back(i);
}
cout<::iterator it = vec1.begin();
for(it;it!=vec1.end();it++)
{
cout<<*it;
}
vec1.pop_back();
cout<
deque(double end queue)容器为一个给定类型的元素进行线性处理,像向量一样,它能够:
一、优点:
快速地随机访问任一个元素 ,即支持[ ]以及at(),但是性能没有vector好。
高效地插入和删除容器的尾部元素 ,但性能不及list。
高效插入和删除容器的头部元素 ,因此也叫做双端队列。
可以在需要的时候改变自身大小,完成了标准的C++数据结构中队列的所有功能。在vector中那样“因为旧空间不足而重新匹配一块更大的空间,然后复制元素,在释放旧的空间”这种事情在deque中不会发生。也因此,deque没有必要提供所谓的空间预留(reserved)功能。
deque可以包含更多的元素,其max_size可能更大,因为不止使用一块内存。
二、 缺点 :
在中间插入元素则会比较费时。
迭代器不是普通指针,是特殊的智能指针,它需要在不同的块中跳转。
deque的元素存取和迭代器操作会稍微慢一些,因为deque的内部结构多了一个间接过程,,效率不如vector。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后(利用STL的sort算法),再复制回deque。
deque不支持对容量和内存分配时机的控制。
在除了首尾两端的其他地方插入和删除元素,都将会导致指向deque元素的任何pointers、references、iterators失效。不过,deque的内存重分配优于vector,因为其内部结构显示不需要复制所有元素。
deque不提供容量操作:capacity()和reverse(),但是vector可以。
三、 原理
#include // 头文件
deque deq; // 声明一个元素类型为type的双端队列que
deque deq(size); // 声明一个类型为type、含有size个默认值初始化元素的的双端队列que
deque deq(size, value); // 声明一个元素类型为type、含有size个value元素的双端队列que
deque deq(mydeque); // deq是mydeque的一个副本
deque deq(first, last); // 使用迭代器first、last范围内的元素初始化deq
deq[ ]:用来访问双向队列中单个的元素。
deq.front():返回第一个元素的引用。
deq.back():返回最后一个元素的引用。
deq.push_front(x):把元素x插入到双向队列的头部。
deq.pop_front():弹出双向队列的第一个元素。
deq.push_back(x):把元素x插入到双向队列的尾部。
deq.pop_back():弹出双向队列的最后一个元素。
//3.遍历
//下标法
int length = vec1.size();
for(int i=0; i :: iterator iter = vec1.begin();
for( ; iter != vec1.end(); iter++)
{
cout<<*iter;
}
#include
#include
#include
using namespace std;
int main(void)
{
int i;
int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
deque q;
for (i = 0; i <= 9; i++)
{
if (i % 2 == 0)
q.push_front(a[i]);
else
q.push_back(a[i]);
} /*此时队列里的内容是: {8,6,4,2,0,1,3,5,7,9}*/
q.pop_front();
printf("%d\n", q.front()); /*清除第一个元素后输出第一个(6)*/
q.pop_back();
printf("%d\n", q.back()); /*清除最后一个元素后输出最后一个(7)*/
deque::iterator it;
for (it = q.begin(); it != q.end(); it++) {
cout << *it << '\t';
}
cout << endl;
system("pause");
return 0;
}
关于set,必须说明的是set关联式容器。set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都 唯一 ,而且系统能 根据元素的值自动进行排序 (默认按键值升序排列)。应该注意的是set中数元素的值不能直接被改变。
set的含义是集合,它是一个有序的容器,里面的元素都是排序好的支持插入、删除、查找等操作,就像一个集合一样,所有的操作都是严格在log(n)时间内完成,效率非常高。是,set插入的元素不能相同,set默认是自动排序的,**键和值相等且唯一。**元素默认按升序排列。
访问元素的时间复杂度是:O(log n)
特点:
set中的元素都是排好序的
set集合中没有重复的元素
储存同一类型的数据元素(这点和vector、queue等其他容器相同)
根据元素的值自动排列大小(有序性)
每个元素的值都唯一(没有重复的元素)——>可以用来排序
高效的插入删除操作
无法直接修改元素
不能使用下标法,只能使用迭代器
头文件
#include;
set具有迭代器set
定义一个迭代器,名为i 可以把迭代器理解为C语言的指针
begin() ,返回set容器第一个元素的迭代器
end() ,返回一个指向当前set末尾元素的下一位置的迭代器.
clear() ,删除set容器中的所有的元素
empty() ,判断set容器是否为空
max_size() ,返回set容器可能包含的元素最大个数
size() ,返回当前set容器中的元素个数
rbegin() ,返回的值和end()相同
rend() ,返回的值和begin()相同
//
set q; //以int型为例 默认按键值升序
set> p; //降序排列
int x;
q.insert(x); //将x插入q中
q.erase(x); //删除q中的x元素,返回0或1,0表示set中不存在x
q.clear(); //清空q
q.empty(); //判断q是否为空,若是返回1,否则返回0
q.size(); //返回q中元素的个数
q.find(x); //在q中查找x,返回x的迭代器,若x不存在,则返回指向q尾部的迭代器即 q.end()
q.lower_bound(x); //返回一个迭代器,指向第一个键值不小于x的元素
q.upper_bound(x); //返回一个迭代器,指向第一个键值大于x的元素
q.rend(); //返回第一个元素的的前一个元素迭代器
q.begin(); //返回指向q中第一个元素的迭代器
q.end(); //返回指向q最后一个元素下一个位置的迭代器
q.rbegin(); //返回最后一个元素
#include
#include
using namespace std;
int main()
{
set q; //默认按升序排列
q.insert(5);
q.insert(5);
q.insert(5);
cout<<"q.size "<::iterator i;
for( i=q.begin();i!=q.end();i++) //set的遍历
cout<<*i<<" "; //输出1 2 3 4,可见自动按键值排序
cout<> p; //降序排列
p.insert(1);
p.insert(2);
p.insert(3);
p.insert(4);
p.insert(5);
for(i=p.begin();i!=p.end();i++)
cout<<*i<<" ";
cout<
#include
#include
using namespace std;
struct node{
int a,b;
bool operator< (const node W)const
{
return a>W.a; //按a的值升序
}
}t;
int main()
{
set q;
t.a=1;
t.b=2;
q.insert(t);
t.a=4;
t.b=2;
q.insert(t);
t.a=3;
t.b=5;
q.insert(t);
set::iterator i;
for(i=q.begin();i!=q.end();i++)
{
t=*i;
cout<
//声明
set a;
//常用函数以下设 set a,其中a是T类型的set容器。
a.begin() 返回指向第一个元素的迭代器
a.end() 返回指向超尾的迭代器
a.clear() 清空容器a
a.empty() 判断容器是否为空
a.size() 返回当前容器元素个数
//插入
a.insert(x) :其中a为set型容器,x为T型变量
set a={0,1,2,9};
a.insert(6);
for(auto it = a.begin();it != a.end();it++) cout << *it;//输出01269
//删除
a.erase(x):删除建值为x的元素
a.erase(first,second):删除first到second区间内的元素(左闭右开)
a.erase(iterator):删除迭代器指向的元素
//案例
#include
#include
using namespace std;
int main()
{
set a;
for (int i = 10; i > 0; i--)
{
a.insert(i);
a.insert(i);
}
a.insert(11);
set::iterator it = a.begin();
for (it ; it != a.end(); it++)
{
cout << *it << " ";
}
cout << endl;
cout << a.empty() << " " << a.size() << endl;
a.clear();
cout << a.empty() << " " << a.size() << endl;
}
map是STL的一个关联容器,它提供一对一的hash
map以模板(泛型)方式实现,可以存储任意类型的数据,包括使用者自定义的数据类型。Map主要用于资料一对一映射(one-to-one)的情況,map內部的实现自建一颗红黑树,这颗树具有对数据自动排序的功能。在map内部所有的数据都是有序的,后边我们会见识到有序的好处。比如一个班级中,每个学生的学号跟他的姓名就存在著一对一映射的关系。
特点:
自动建立key-value的对应,key和value可以是任何你需要的类型,包括自定义类型
增加和删除节点对迭代器的影响很小(高效的插入与删除)
可以根据key修改value的记录
支持下标[]操作
快速的查找(同set)
头文件:
#include
map对象是模板类,需要关键字和存储对象两个模板参数:
std:map
为了使用方便,可以对模板类进行一下类型定义,
typedef map
UDT_MAP_INT_CSTRING enumMap;
map共提供了6个构造函数,这块涉及到内存分配器这些东西,略过不表,在下面我们将接触到一些map的构造方法,这里要说下的就是,我们通常用如下方法构造一个map:
map mapStudent;
首先,定义一个map对象
map mapStudent;
第一种,使用insert函数插入**pair**数据
mapStudent.insert(pair(000, "student_zero"));
第二种,使用insert函数插入**value_type**数据
mapStudent.insert(map::value_type(001, "student_one"));
第三种,用array方式插入
mapStudent[123] = "student_first";
mapStudent[456] = "student_second";
以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的,但是用数组方式就不同了,它可以 覆盖以前该关键字对应的值 ,用程序说明如下:
mapStudent.insert(map::value_type (001, "student_one"));
mapStudent.insert(map::value_type (001, "student_two"));
上面这两条语句执行后,map中001这个关键字对应的值是“student_one”,**第二条语句并没有生效,**那么这就涉及到我们怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功,程序如下:
// 构造定义,返回一个pair对象
pair insert (const value_type& val);
pair
我们通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。
当所查找的关键key出现时,它返回数据所在对象的位置,如果沒有,返回iter与end函数的值相同。
//find返回迭代器指向当前查找元素的位置否则返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
cout<<"Find, the value is"<second<
迭代器删除
iter = mapStudent.find("123");
mapStudent.erase(iter);
关键字删除
int n = mapStudent.erase("123"); //如果刪除了会返回1,否则返回0
使用迭代器清空
mapStudent.erase(mapStudent.begin(), mapStudent.end());
使用关键字清空
mapStudent.clear();
int nSize = mapStudent.size();
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数, (帮助评论区理解: 因为key值不会重复,所以只能是1 or 0)
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
(*it)->first key
(*it)->second value
#include
#include
stack翻译为 栈 。
后进先出 , 先进后出
不允许遍历
特点:
由于栈是一种有序列表,故可以使用数组模拟。
头文件:
#include;//注意,STL头文件没有扩展名.h
map对象是模板类,需要关键字和存储对象两个模板参数:
stack name;//这样就定义了一个用int作为索引,并拥有相关联的指向string的指针.
由于栈(stack)本身就是一种后进先出的数据结构,在STL的 stack中只能通过top()来访问栈顶元素
#include
#include
using namespace std;
int main(){
stack st;
for(int i=1;i<=5;i++){
st.push(i); //push(i)将i压入栈
}
printf("%d\n",st.top()); //top()取栈顶元素
return 0;
}
push()
push(x)将x入栈,时间复杂度为O(1),实例见“ stack容器内元素的访问”。
top()
top()获得栈顶元素,时间复杂度为O(1),实例见“ stack容器内元素的访问”。
pop()
pop()用以弹出栈顶元素,时间复杂度为O(1)。
#include
#include
using namespace std;
int main(){
stack st;
for(int i=1;i<=5;i++){
st.push(i); //push(i)将i压入栈 ,1 2 3 4 5 依次入栈
}
for(int i=1;i<=3;i++){
st.pop(); //pop()将栈顶元素出栈,即将5 4 3 依次出栈
}
printf("%d\n",st.top()); //top()取栈顶元素
return 0;
}
运行结果:
empty()
empty()可以检测stack是否为空,返回true为空,返回false为非空,时间复杂度为O(1)。
程序代码:
#include
#include
using namespace std;
int main(){
stack st;
printf("%d\n",st.empty()); //true=1;false=0
for(int i=1;i<=5;i++){
st.push(i); //push(i)将i压入栈 ,1 2 3 4 5 依次入栈
}
printf("%d\n",st.empty()); //true=1;false=0
return 0;
}
运行结果:
size()
size()返回stack内元素的个数,时间复杂度为O(1)。
程序代码:
#include
#include
using namespace std;
int main(){
stack st;
for(int i=1;i<=5;i++){
st.push(i); //push(i)将i压入栈 ,1 2 3 4 5 依次入栈
}
printf("%d\n",st.size());
return 0;
}
运行结果:
注意:
在使用pop()和top()函数之前必须先使用empty()函数判断栈是否为空。
STL链表是序列性容器的模板类,它将其元素保持在线性排列中,链式结构,并允许在序列中的任何位
置进行有效的插入和删除。
优点:
与向量(vector)相比, 它允许快速的插入和删除。
时间复杂度O(1)
动态分配内存,不会造成内存的浪费和溢出
缺点:
遍历慢,
随机访问却比较慢,
占用空间大,
查找的效率为O()
List是stl实现的双向且循环的链表。
迭代器,只支持前移和后移,属于双向迭代器。
性质:Lise插入和删除操作不会令原有的list迭代器失效,这在vector中是不成立的。
list lst;//list采用模板类实现,对象的默认构造形式
list(beg,end);//构造函数将[beg,end]区间中的元素给本身
list (n,elem);//构造函数将n个elem拷贝给本身
list(const list &lst);//拷贝构造函数
#include
using namespace std;
#include
//list函数构造函数
void printList(const list&L)
{
for(list::const_iterator it = L.begin();it!=L.end();it++)
{
cout<<*it<<" ";
}
}
void test01()
{
//创建list函数
listL1;//默认函数
//添加数据
L1.push_back(10);
L1.push_back(20);
L1.push_back(30);
L1.push_back(40);
L1.push_back(50);
printList(L1);
}
int main ()
{
test01();
system("pause");
return 0;
}
assign(beg,end);//将[beg,end]区间的数据赋值给本身
assign(n,elem);//将n个elem拷贝赋值给本身
list& operator = (const list &lst);//重载符号操作符
swap(lst);//将lst与本身的元素互换
#include
using namespace std;
#include
//list函数构造函数
void printList(const list&L)
{
for(list::const_iterator it = L.begin();it!=L.end();it++)
{
cout<<*it<<" ";
}
cout<L1;//默认函数
//添加数据
L1.push_back(10);
L1.push_back(20);
L1.push_back(30);
L1.push_back(40);
L1.push_back(50);
printList(L1);
listL2;
L2=L1;//operator= 赋值
printList(L2);
listL3;
L3.assign(L2.begin(),L2.end());
printList(L3);
listL4;
L4.assign(10,100);
printList(L4);
}
int main ()
{
test01();
system("pause");
return 0;
}
begin()返回的是第一个元素的指针(可以理解为是指针),但是end()返回的不是最后一个元素的指针而是最后一个元素下一个位置的指针。仅仅用作标记结尾的作用,是无效节点
//cout << *lst.end() << " ";//运行会出错
cout << *--lst.end() << endl;
front() 获取头节点元素
back() 获取尾节点元素
size() 获取长度
empty() 判空
clear()
C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。
auto 关键字基本的使用语法如下:
auto name = value;
单独使用
#include
#include
#include
using namespace std;
int main() {
int x = 0; //
auto b = x; // b的类型自动推导为int
auto c = &x; // c的类型自动推导为int*
auto d = 12.3; // d的类型自动double
return 0;
}
g++ -std=c++11 auto.cpp -o auto.out
组合使用
#include
#include
#include
using namespace std;
int main() {
int x = 0;
//组合使用
auto *p1 = &x; // p1 为 int *,auto 推导为 int
auto p2 = &x; // p2 为 int*,auto 推导为 int*
auto &r1 = x; // r1 为 int&,auto 推导为 int
auto r2 = r1; // r2 为 int,auto 推导为 int
return 0;
}
auto 和const使用
#include
#include
#include
#include
using namespace std;
int main() {
int x = 0;
const auto n = x; // n 为 const int ,auto 被推导为 int
auto f = n; // f 为 const int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x; // r1 为 const int& 类型,auto 被推导为 int
auto &r2 = r1; // r1 为 const int& 类型,auto 被推导为 const int 类型
return 0;
}
auto 与 const 结合的用法:
#include
using namespace std;
int main(){
vector< vector<int> > v;
vector< vector<int> >::iterator i = v.begin();
return 0;
}
#include
using namespace std;
int main(){
vector< vector<int> > v;
auto i = v.begin(); //使用 auto 代替具体的类型
return 0;
}
#include
using namespace std;
class A{
public:
static int get(void){
return 100;
}
};
class B{
public:
static const char* get(void){
return "http://c.biancheng.net/cplus/";
}
};
template <typename T>
void func(void){
auto val = T::get();
cout << val << endl;
}
int main(void){
func<A>();
func<B>();
return 0;
}
1) 使用 auto 的时候必须对变量进行初始化
template <typename T>
class A{
//TODO:
};
int main(){
A<int> C1;
A<auto> C2 = C1; //错误
return 0;
}
decltype 是 C++11 新增的一个关键字,它和 [[auto]] 的功能一样,都用来在编译时期进行自动类型推导。
decltype 是“declare type”的缩写,译为“声明类型”。
既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。
auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
auto varname = value;
decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。
auto 根据=
右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=
右边的 value 没有关系。
这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;
原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;
例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。
1. 如果 exp 是一个不被括号`( )`包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
( )
包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。[[左值和右值]]
using namespace std;
class Base{
public:
int x;
};
int main(){
const Base obj;
//带有括号的表达式
decltype(obj.x) a = 0; //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
decltype((obj.x)) b = a; //obj.x 带有括号,符合推导规则三,b 的类型为 int&。
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0; //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
decltype(n = n + m) d = c; //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&
return 0;
}
auto 的语法格式比 decltype 简单,所以在一般的类型推导中,使用 auto 比使用 decltype 更加方便
auto 只能用于类的静态成员,不能用于类的非静态成员(普通成员),如果我们想推导非静态成员的类型,这个时候就必须使用 decltype 了。
#include
using namespace std;
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin();
}
private:
typename T::iterator m_it; //注意这里
};
int main()
{
const vector<int> v;
Base<const vector<int>> obj;
obj.func(v);
return 0;
}
单独看 Base 类中 m_it 成员的定义,很难看出会有什么错误,但在使用 Base 类的时候,如果传入一个 const 类型的容器,编译器马上就会弹出一大堆错误信息。原因就在于,T::iterator
并不能包括所有的迭代器类型,当 T 是一个 const 容器时,应当使用 const_iterator。
要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:
template <typename T>
class Base {
public:
void func(T& container) {
m_it = container.begin();
}
private:
decltype(T().begin()) m_it; //注意这里
};
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int main()
{
int x1 = 1;
//我们知道有不加等号这种写法即可,自己写的时候最好还是要加上
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
同时也可以用来初始化对象:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是右值?
左值可以取地址,可以出现在等号左边,可以改变值的大小(const类型除外)。
右值不可以被取地址,不可以出现在等号左边,只能出现在等号右边,不可以改变值的大小。右值一般有两种,一种是纯右值,一种是将亡值(函数返回的临时变量)。
左值引用总结:
#include
#include
#include
#include
using namespace std;
int main() {
//左值引用只能引用左值
int a = 10;
int &ra1 = a; // ra1 为a的别名
// int& ra2 =10;//cannot bind non-const lvalue reference of type ‘int&’ to an
// rvalue of type ‘int’ 不能将左值引用和右值绑定
// const左值引用既可以引用左值,也可以引用右值
const int &ra3 = a;
const int &ra4 = 10;
return 0;
}
右值引用总结:
#include
#include
#include
#include
using namespace std;
int main()
{
//右值引用只能引用右值
int&& r1 =10;
int a=10;
//int&& r2 = a;//error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
//右值引用可以引用move之后的左值
int&& r3 =move(a);
return 0;
}
需要注意的是,右值虽然不能被取地址,但右值被取别名后会存储到特定的位置,这个特定位置的地址是可以被取地址的。也就是说字面量10为右值不能被取地址,但是右值引用取别名为r1后,r1的地址是可以取到的,并且r1的值是可以被修改的,如果不想rr1被修改,可以用const int&& r1 去引用。当然右值引用的实际用处并不在此,只需要了解即可。
动态库和静态库
团队之间的协作,只提供给函数接口,提供库,隐藏了函数的实现路逻辑
注意:静态和动态磁盘中都存很多份,没区别。
链接期把库放入内存
优点:
快
缺点:
链接期只放入库的索引
缺点:
慢
优点:
在测试项目中
1、把头文件和库文件(.lib)拷贝到当前项目中
2、添加依赖的头文件
3、导入依赖库
4、生成.exe
5、将动态库生成的.dll文件放入和exe同一目录
6、调用运行
后续对动态库的函数实现进行更改,只需要将测试项目中的dll文件更新就可以了,而不用重新编译项目文件