个人通过学习C++,手打出了一份27000字C++笔记,包括封装继承多态等面向对象的思想;笔记中包含了大量的代码案例对每个重要的知识点进行了代码演示,通过理论和实操的结合,更好的透析每个知识点。
本文内容较长,包含的知识点很多,建议使用Ctrl+f 来查找知识点来学习。
内容中的源代码都可以在我的github上下载:https://github.com/jiong1998/Cplusplus.github.io
注:本人运行环境mac + clion + C++14
//假设项目名叫unix_networks,项目中主文件叫main.c
cmake_minimum_required(VERSION 3.21)//clion默认,不管
project(unix_networks C)//clion默认,不管
set(CMAKE_C_STANDARD 99)//clion默认,不管
#声明头文件的路径
set(INC_DIR ./)
#声明链接库的路径
set(LINK_DIR ./)
#引入头文件
include_directories(${INC_DIR})
#引入库文件
link_directories(${LINK_DIR})
add_executable(unix_networks main.c)
add_executable(client client.c)
target_link_libraries(unix_networks libunp.a)
target_link_libraries(client libunp.a)
//client.c是想要运行的可执行文件
add_executable(client client.c)
target_link_libraries(client libunp.a)//把库文件链接上
::代表作用域 如果前面什么都不添加 代表全局作用
using声明:声明接下来的代码我都将使用这个作用于下的这个变量,using KingGlory::sunwukongId。
using编译指令:
C语言下const修饰的全局变量默认是外部链接属性,可以直接在本项目的其他文件调用。
C++下const修饰的全局变量默认是内部链接属性,需要添加extern才能给别的文件调用
int& aRef = a;
//自动转换为 int* const aRef = &a;这也能说明引用为什么必须初始化
c++从c中继承的一个重要特征就是效率。假如c++的效率明显低于c的效率,那么就会有很大的一批程序员不去使用c++了。在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function). 内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。内联函数是直接跑源码,不通过函数的调用。
inline int func(int a){return ++;}
函数的声明和实现都必须加关键字inline,才算内联函数。内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
注:类内部定义函数都会自动成为内联函数
内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
void TestFunc01(int a,int b,int)
通过函数重载的条件,可以同时写多个同名函数,生成不同效果。
函数重载的条件:
extern “C”:由于C++对每个函数都会重新取名字(函数重载的原理),但是C不会,所以当我们在C++想引用C写的函数时,我们就得加上**extern “C”**告诉编译器用C语言方式作链接。
在C的文件中加入:
c++中struct也可以使用函数,他们的唯一区别:
protected和private的区别:子类不可以访问父类private的内容,但是可以访问父类protected的内容。
尽量将成员变量设置成私有,设置公共接口来让别人设置:好处是自己可以控制读写的权限,并且可以对设置进行有效性的验证。
对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,分别完成对象初始化和对象清理工作。
1. 括号法:Person p1(10);// Person p1;
2. 显示法: Person p1=Person(10);//Person p1=Person();
3. 指针法: Person *p1 = new Person(10);//Person *p1 = new Person();
4. 隐式法: Person p1=10;//一般不用
注:不要用括号法调用无参构造函数 Person p3(); 编译器认为代码是函数的声明。改成:Person p3=Person()
匿名对象:Person(10);
特点:执行完立即释放
注意,拷贝构造函数的输入参数是const引用类型
Person(const Person & p);//这是一个拷贝构造函数
//拷贝构造函数的语法
class Person
{
public:
Person(char * name, int age)//构造函数
{
m_name=(char *) malloc(strlen(name)+1);
strcpy(m_name,name);
m_age=age;
}
Person(const Person & p)//拷贝构造函数
{
m_name=(char *) malloc(strlen(p.m_name)+1);
strcpy(m_name,p.m_name);
m_age=p.m_age;
}
~Person()
{
cout<<"析构函数已调用"<<endl;
free(m_name);
m_name=NULL;
}
int m_age;
char * m_name;
};
void test3()
{
Person p1=Person("卢锦荣",20);
Person p2=p1;
cout<<p1.m_name<<"\n"<<p1.m_age<<endl;
cout<<p2.m_name<<"\n"<<p2.m_age<<endl;
}
当你利用系统默认的拷贝构造函数时,可能会出错:
类中有成员是指针变量,并且指针指向动态分配的内存空间,当你用系统默认的拷贝构造,会把两个对象的同一个成员指向同一个堆区(浅拷贝),所以当析构函数释放内存时,会把一个堆区释放两次
构造函数名称(值1,值2):属性1(值1), 属性2(值2)
//传统方式初始化
Person(int a,int b,int c){
mA = a;
mB = b;
mC = c;
}
//初始化列表方式初始化
Person(int a, int b, int c):mA(a),mB(b),mC(c){}//mA、mB、mC为成员变量,abc为用于输入的值
当其他类对象作为本类成员,会先构造其他类对象,再构造自身,析构顺序和构造顺序相反(栈的原理)
禁止用户调用构造函数的隐式调用。
隐式法: Person p1=10;
new:在堆区开辟一片空间(和malloc一样)
c++不用malloc,用new。
new会调用构造函数,malloc不会
new返回的是地址,要用指针类型接收。
堆区开辟对象,一定会调用对象的无参构造函数,如果对象存在有参构造函数,而没有无参构造函数,则会报错(因为一定会调用对象的无参构造函数,而你没有)。
利用new来创建n个数组对象:
Person* person = new Person[10];
delete person;//error
delete [] person;//对!如果在new表达式中使用[],必须在相应的delete表达式中也使用[]
static(静态):
1. 被static修饰的类型,空间将在程序的生命周期内分配。(记住这个结论!很关键,不管是变量,对象的变量,对象的函数都是如此)
2. 还可以限定访问范围 static还有限定访问范围的作用(类似于匿名名字空间),用得少。
1. 静态成员变量:
2. 静态成员函数:
一个类只能实例化出一个对象,称单例模式。具体实现:
class ChairMan
{
public:
static ChairMan * GetInstance()//利用接口提供可读。
//注意:静态成员变量只能通过静态成员函数调用
{
return singleMan;
}
private:
ChairMan(){}//将构造函数私有化,不允许创建对象
ChairMan(const ChairMan & ){}//将拷贝构造函数私有化,不允许创建对象
static ChairMan * singleMan;//放在private中,为了限制可读不可写
};
ChairMan* ChairMan::singleMan=new ChairMan;//给指针类型为主席的赋予一个空间。注:静态成员变量只能全局初始化
void test2()
{
ChairMan * c1=ChairMan::GetInstance();
}
通过上例我们知道,c++的数据和操作也是分开存储,并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。
那么问题是:这一块代码是如何区分那个对象调用自己的呢?利用this指针。
this指针指向被调用的成员函数所属的对象。
public:
Person(int age)
{
age=age;//error
this->age = age;//正确写法
}
int age;
this指针的本质:
Person * const this;
即 const修饰指针,所以this不可以更改!而指针this指向的值可以修改
什么是空指针:
Person*p=NULL;//或者 Person p;
如果成员函数中没有用到this指针,可以用空指针调用成员函数
4.5 结尾说到:即 const修饰指针,所以this不可以更改!而指针this指向的值可以修改。
但是如果想某个类的函数不允许修改成员变量该怎么办(即指针this指向的值也不可以修改)? 使用常函数
//某个类中的函数
class Person
{
public:
void show_age() const //常函数修饰成员函数中的this指针,让指针指向的值不可以修改
{
this->m_age=10;//error
cout<<"age"=<<this->m_age<<endl;
}
int m_age;
}
常对象同理,构造对象时,给对象加上const属性,对象的成员变量就不可以更改。并且常对象只能调用常函数,不允许调用普通函数(因为普通函数可能会修改成员变量的值,所以不允许常对象调用普通函数)。
const Person p=Person();
但是如果你想在常函数或者常对象中,让某些特殊的属性仍然可以更改,可以给变量加上关键字mutalbe
mutable int m_A
//类内声明,类外实现
class Person1
{
public:
Person1(int age);//构造函数
void change_age(int age);
void show_age();
int M_age;
};
//构造函数的实现
Person1::Person1(int age)
{
this->M_age=age;
}
void Person1::change_age(int age)
{
this->M_age=age;
}
void Person1::show_age()
{
cout<<"age="<<M_age<<endl;
}
void test4()
{
Person1 p=Person1(11);
p.show_age();
}
对于普通类,如果我们想分文件来声明与实现,一般是
头文件person.h(假设类名是Person)中声明,
person.cpp中 #include “person.h” 并 实现函数。
在主要运行程序 main.c中 #include “person.h”, 并且在编译的时候
g++ -o main main.cpp person.cpp -I./
注意:-I(i的大写) 为指定头文件所在的路径
代码实例:
person.h
//.h文件只需要声明
#include
using namespace std;
class Person
{
public:
Person(string name, int age);
void showPerson();
string m_name;
int m_age;
};
person.cpp
//.cpp文件只需要实现
#include "person.h"
Person::Person(string name, string age)
{
this->m_name=name;
this->m_age=age;
}
void Person::showPerson()
{
cout<<"姓名"<<this->m_name<<"年龄"<<this->m_age<<endl;
}
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,怎么办?
解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。
全局函数、某个类中的成员函数、甚至整个类声明为友元。
三种类型声明为友元的例子:
class A{}//类做友元的例子
class B//类中某个成员函数做友元例子
{
void visit();//可以访问building私有
void visit2();//不可以访问building私有
}
class Building//主要做例子的类
{
//利用friend关键字让全局函数 goodGay作为本类的友元函数,可以访问私有属性m_BedRoom
friend void goodGay(Building * buliding);
//类做友元
friend class A;//A是一个类
//类中成员函数做友元
friend void B::visit();
private:
string m_BedRoom; //私有成员变量
public:
Building()//无参构造函数
{
this->m_BedRoom = "卧室";
}
};
void goodGay(Building * buliding)
{
cout << buliding->m_BedRoom << endl;
}
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
注意: 运算符重载只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字是关键字operator@,这里的@代表了被重载的运算符。
可以全局函数定义,也可以类内定义。
Person operator+(Person &p1,Person&p2)
{
Person temp;
temp.m_A=p1.m_A+p2.m_A;
temp.m_B=p1.m_B+p2.m_B;
return temp;
}
Person p3=p1+p2;//本质:Person p3 = operator+(p1,p2)
左移运算符作cout输出操作。对于想要输出对象的成员变量的重载<<,只能使用全局函数定义。
//重载左移运算符,使其能输出对象的每个成员变量。
ostream & operator<<<<(ostream &cout ,Person &p1)
{
cout<<p1.m_A<<p1.m_B;
return cout;
}
Person p1;//构造一个对象p1
cout<<p1<<endl;
对对象中的成员变量做前置++操作。
class MyInter
{
private:
int m_Num;
public:
MyInter()//构造函数
{
m_Num=0;
}
MyInter & operator++()//重载前置++运算符
{
this->m_Num++;
return *this;
}
};
MyInter MyInt;//构造一个对象MyInt
++MyInt;//重载前置++运算符
对对象中的成员变量做后置++操作。后置++比前置++复杂
MyInter operator++(int)//int为占位参数,不代表具体含义,但是可以让编译器知道是后置++
{
//思路: 先保存修改前的变量,然后++,返回修改前的变量。
MyInter temp=*this;
this->m_Num++;
return temp;//注意 返回的事值,不能是引用,原因比较复杂
}
MyInter MyInt;//构造一个对象MyInt
MyInt++;//重载前置++运算符
重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象,也叫仿函数(functor),其实就是重载“()”操作符,使得类对象可以像函数那样调用。一般函数对象只会重载(),不做其他用途。
本质上就是一个类的对象重载了小括号。
class MyPrint
{
public:
void operator()(string str)
{
cout<<str<<endl;
}
};
void test3()
{
MyPrint myPrint;
myPrint("heelo");//重载函数调用运算符,称仿函数
}
总结:
继承就是把共同的共性的东西做父类,子类中额外构建一些自己的特性。避免代码的冗余。
//基本语法:class 子类:继承方式 父类
class News: public BasePage//BasePage是父类,News是子类
{
public:
void content(){};//自己除了拥有父类所有的功能外,自己新增加的功能。
}
三种继承方式:
父类中的私有属性,子类其实继承了,只是被编译器隐藏了,子类访问不到,实际上私有属性还是会占用地址空间。
父类的构造/析构函数是不会被子类继承的, 只会在构造子类的时候先调用父类的继承/析构函数 。
请注意:
子类继承中:
先调用父类的构造函数,再调用子类的构造函数(先有爸爸再有儿子)。析构顺序与构造相反(栈)
进一步的,假设子类中调用了其他类:
先调用父类的构造函数,再调用其他类的构造函数,最后调用子类的构造函数。(注意,其他类的构造函数一定优先于本类构造函数)
注意:当子类不存在有参构造函数,而只有无参构造函数,而父类中只存在有参构造函数,而不存在无参构造函数。构造子类对象时会报错(因为会调用父类的无参构造函数),有两种解决方法:
class Base
{
public:
Base(int a)
{
this->m_A = a;
cout << "这是一个有参构造函数" << endl;
}
int m_A;
};
class Son :public Base
{
public:
Son2(int a) :Base2(a) //利用初始化列表,调用父类的有参构造函数
{
cout << "这是子类的构造函数" << endl;
}
};
Son son=Son(100);
父类中的构造、析构、拷贝构造、operator=都不会被子类继承。
同名成员包括:成员变量、成员函数。
当子类成员和父类成员同名时,子类依然从父类继承同名成员。只是访问父类同名成员的方式不同了:
void test1()
{
Son s1;//构造一个子类
cout<<s1.m_A<<endl;//子类的同名成员变量
cout<<s1.Base::m_A<<endl;//父类的同名成员变量
s1.fun1();//子类的同名成员函数
s1.Base::fun1();//父类的同名成员函数
}
此外对于同名函数而言:如果子类重新定义了父类的同名成员函数,那么子类就会自动隐藏掉父类中所有重载版本的同名成员,但是还是可以通过作用域的方式来调用。(这行话如果看不太懂就不看了,反正意思就是调用父类同名成员都得加上作用域,不管有没有父类是否重载;但是如果不同名,不需要作用域就可以直接调用)
关于static同名静态成员的特点:
和前面的结论其实一样,也是要通过 s1.Base::变量/函数 来调用;
但是静态成员因为是单独分配内存(静态成员的空间将在程序的生命周期内分配),所以可以直接通过类名调用。
Son s1=Son()
Son.m_A;//调用子类同名静态变量
s1.m_A;//和上面一样
Base.m_A//调用父类同名静态变量
s1.Base::m_A//调用父类同名静态变量
//同名成员函数不再做示范,同理。
多继承概念:
一个子类可以拥有多个父类。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义,只能利用声明作用域的方式解决。
菱形继承的概念:
两个派生类继承同一个基类,而又有某个类继承这两个类,这种继承被称为菱形继承,或者钻石型继承。
这种菱形继承带来的问题:
解决方法:
利用虚继承(virtual)解决菱形继承问题
//动物类
class Animal
{
public:
int m_Age; // 年龄
};
//羊类
class Sheep : virtual public Animal{};
//驼类
class Tuo : virtual public Animal{};
//羊驼类
class SheepTuo : public Sheep, public Tuo
{
};
//此时如果不用虚继承,那么羊驼类会存在两个m_Age成员
当发生虚继承后,sheep和tuo类中继承了一个 vbptr指针—虚基类指针 ,指向的是一个 虚基类表 vbtable。虚基类表中记录了 偏移量 ,通过偏移量 可以找到唯一的一个m_Age,具体来说,利用地址偏移找到 vbtable中的偏移量 并且访问数据
虚继承:这是C++中一种特殊的继承方式,用于解决多重继承中的菱形继承问题。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类,本例中的 动物 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
c++支持静态多态(编译时多态)和动态多态(运行时多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编):
此外:有父子关系的两个类的指针或者引用,是可以直接转换的,不需要我们人为转换。也就是可以直接父类的引用接收子类的对象
void doSpeak(Animal & animal)
{
animal.speak();
}
Cat cat;//Cat为Animal的子类
doSpeak(cat);//正确。但是调用的是Animal::speak,而不是Cat::speak
重点:
由于父类指针或者引用能指向子类对象,所以上述代码会产生一个问题:他输出的是父类的函数,而不是子类的函数,造成这个问题的原因是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。 编译器根据指向对象的指针或引用的类型来选择函数调用 。这个时候由于doSpeak的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Cat::speak。
解决此方法是通过虚函数virtual,由此产生了多态。
多态产生的条件:
注意:重写的定义:子类重写父类中的虚函数,函数返回值相同,形参相同
总结:
当父类写了虚函数后,类的内部结构发生了改变,多了一个叫vfptr虚函数表指针,指针指向vftable虚函数表。虚函数表内部记录着虚函数的入口地址。当父类的指针或者引用指向子类对象时,发生多态。调用虚函数的时候从虚函数表中找到函数的入口地址。如果子类重写了父类的虚函数,那么虚函数表中原本记录着父类的函数入口地址将被替换成子类的函数入口的地址。
class Animal
{
public:
virtual void speak()//虚函数
{
cout<<"动物在说话"<<endl;
}
};
class Cat:public Animal
{
public:
//子类重写了父类的虚函数。所以当父类指针指向子类对象时,
//虚函数表中的函数入口地址是该地址,而不是父类的函数地址。
//子类的speak()函数的virtual可加可不加
void speak()
{
cout<<"小猫在说话\n"<<endl;
}
};
void test1()
{
Animal * animal=new Cat;//父类指针指向子类对象
animal->speak();
}
输出结果:
小猫在说话
Process finished with exit code 0
案例:用多态实现两数相加或者相减(案例看懂他,就能明白多态的意义)
class AbstractCalculator //父类
{
public:
virtual int getResult()//抽象了一个函数,留着给子类继承
{
return 0;
}
}
//父类指针指向子类对象,并且子类重写了父类的虚函数,发生多态!
AbstractCalculator * calculator=new AddCalculator;//加法类,继承自AbstractCalculator
calculator.getResult(1,5);//结果为6
delete calcultor;//清空加法对象的空间
//父类指针指向子类对象,并且子类重写了父类的虚函数,发生多态!
calculator=new SubCalculator;//减法类
calculator.getResult(1,5);//结果为-4
这就是多态的意义:当用同样的父类指针指向不同的子类对象时,调用同样的函数,输出却是完全不同的,这就是多态的意义。
这个意义就是所提倡的设计原则:开闭原则,对扩展进行开放,对修改进行关闭。
所以多态的好处:
在1.1 最后一个案例《用多态实现两数相加或者相减》中,给父类的虚函数写了一段对这个函数函无意义的代码(return 0;)。其实我们根本没有必要写这个“return 0;”,只需要改成纯虚函数就行。
纯虚函数: 纯虚函数不需要写实现,只需要提供接口。
class A//抽象类
{
public:
virtual int getResult()=0;//纯虚函数
}
如果一个类中包含了纯虚函数,那么该类就无法实例化对象。 该类通常被称为抽象类。
抽象类的子类,必须重写父类的纯虚函数,否则也属于抽象类。
纯虚函数的意义在于:继承抽象类的子类必须要重写纯虚函数,不然就没办法实例化对象!
这个纯虚函数被称为公共的接口。
在设计时,常常希望基类仅仅作为其派生类的一个接口,而不希望用户实际的创建一个基类的对象。
多态中抽象类与纯虚函数的实际案例:
假设要设计两套动作:冲咖啡和冲茶叶,其实整体流程大同小异,只是具体实现上有细微差别。
//抽象制作饮品
class AbstractDrinking//抽象类
{
public:
//烧水
virtual void Boil() = 0;//纯虚函数
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink(){
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking{
public:
//烧水
virtual void Boil(){//这里写不写virtual都可以。
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking{
public:
//烧水
virtual void Boil(){
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入食盐!" << endl;
}
};
//业务函数
void DoBussiness(AbstractDrinking* drink){
drink->MakeDrink();
delete drink;
}
void test(){
DoBussiness(new Coffee);//多态的实现
cout << "--------------" << endl;
DoBussiness(new Tea);//多态的实现
}
父类的构造/析构函数是不会被子类继承的, 只会在构造子类的时候先调用父类的继承/析构函数 。
为什么需要虚析构:
当使用多态(前提)时,即父类指针指向子类对象;当delete时,父类指针只会调用父类析构函数,不会调用子类析构函数。因此需要将父类的析构函数变为虚析构函数,以此才调用子类的析构函数。
class Animal1//父类
{
public:
virtual void speak()
{
cout<<"动物在说话"<<endl;
}
virtual ~Animal1()//虚析构函数。
{
cout<<"Animal1的析构函数调用"<<endl;
}
};
纯虚析构和其他的纯虚函数不同,除了有声明(~A()=0),他还需要实现,所以类内声明,类外实现。
class Animal1//父类
{
public:
virtual void speak()
{
cout<<"动物在说话"<<endl;
}
virtual ~Animal1()=0;//纯虚析构函数。
};
Animal1::~Animal1()//类内声明,类外实现。
{
cout<<"Animal1的析构函数调用"<<endl;
}
注意:如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。
父转子: 向下类型转换 不安全(地址越界)
子转父: 向上类型转换 安全
如果发生多态,那么转换永远都是安全的
结合这张图理解。
重新回顾下三个定义
案例分析:假设一个电脑由三个零件组成,内存显卡CPU,三个零件可能由不同厂商构成,所以需要我们构建三个父类的抽象类,只需要提供接口(虚函数)即可。
具体代码实现
//抽象化三个类
class CPU
{
public:
virtual void calculate()=0;
};
class VideoCard
{
public:
virtual void display()=0;
};
class Memory
{
public:
virtual void storage()=0;
};
//电脑类
class Computer
{
public:
Computer(CPU * cpu, VideoCard* videocard, Memory * memory)
{
m_cpu=cpu;
m_videocard=videocard;
m_memory=memory;
}
CPU *m_cpu;
VideoCard * m_videocard;
Memory * m_memory;
void dowork()
{
m_cpu->calculate();
m_videocard->display();
m_memory->storage();
}
};
//定义不同厂商CPU、显卡、内存条
class InterCPU:public CPU
{
virtual void calculate()
{
cout<<"这是因特尔cpu在计算"<<endl;
}
};
class AppleCPU:public CPU
{
virtual void calculate()
{
cout<<"这是苹果cpu在计算"<<endl;
}
};
class InterVideoCard:public VideoCard
{
virtual void display()
{
cout<<"这是因特尔显卡在显示"<<endl;
}
};
class AppleVideoCard:public VideoCard
{
virtual void display()
{
cout<<"这是苹果显卡在显示"<<endl;
}
};
class InterMemory:public Memory
{
virtual void storage()
{
cout<<"这是因特尔内存在存储"<<endl;
}
};
class AppleMemory:public Memory
{
virtual void storage()
{
cout<<"这是苹果内存在存储"<<endl;
}
};
void test4()
{
//苹果配件
VideoCard * appleVC=new AppleVideoCard;
CPU * applecpu=new AppleCPU;
Memory * applememory= new AppleMemory;
//Inter配件
VideoCard * interVC=new InterVideoCard;
CPU* intercpu=new InterCPU;
Memory * intermemory=new InterMemory;
Computer c1 =Computer(applecpu,interVC,applememory);
c1.dowork();
}
运行结果:
这是苹果cpu在计算
这是因特尔显卡在显示
这是苹果内存在存储
c++提供两种模板机制:函数模板和类模板
c++提供了函数模板(function template.)所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
//利用模板实现通用交换函数
template <typename T>//T代表一个通用的数据类型
//typename也可以用class
void mySwap(T&a,T&b)
{
T temp=a;
a=b;
b=temp;
}
void test1()
{
int a=10,b=20;
//1、自动类型推导,必须推导出一致的数据类型,才可以使用模板
mySwap(a,b);
//2、显示指定类型
mySwap<int>(a,b);
}
两种方式使用函数模板:
注意:template
泛型编程 – 模板技术 特点:类型参数化
当写了一个函数模板,又重载了一个普通函数时,就会发生函数模板和普通函数都存在的情况。
template <class T>
void myPrint(T a, T b){}
void myPrint(int a, int b){}
区别:
函数模板:如果使用自动类型推导,是不可以发生隐式类型转换的(char-> int)
普通函数:可以发生隐式类型转换
调用规则:
函数模板机制结论:
前面结论说到,编译器并不是把函数模板处理成能够处理任何类型的函数。比如交换两个数的函数模板,如果传入的是结构体,或者数组,甚至一个类,模板就解决不了。这就是模板的局限性
那么如何解决模板的局限性呢?
为了解决这种问题,可以利用重载 或者具体化技术,为这些特定的类型提供具体化的模板。
具体化技术:
template<> bool myCompare(Person & p1, Person & p2);
//其实不加template<>好像也可以
类模版与函数模板的区别:
//类模板
template <class T1, class T2>
//template //可以有默认参数
class Person1
{
public:
Person1(T1 name, T2 age)
{
this->m_name=name;
this->m_age=age;
}
void showPerson()
{
cout<<"名字"<<this->m_name<<this->m_age<<endl;
}
T1 m_name;
T2 m_age;
};
void test3()
{
Person1<string, int> p1=Person1<string, int>("小卢",13);//显示指定类型
//Person1 p1=Person1("小卢",13);//有默认参数情况下的
cout<<p1.m_name<<endl;
}
类模板的成员函数不是一开始创建好的,只有在运行阶段,确定了T的数据类型,才会创建。
当dowork函数想调用类模板作为函数的参数,有以下三种方式
//类模板
template <class T1, class T2>
class Person1
{
public:
Person1(T1 name, T2 age)
{
this->m_name=name;
this->m_age=age;
}
T1 m_name;
T2 m_age;
void showPerson(){}
};
//1 指定传入类型(最常用)
void doWork(Person <string, int>&p)
{
p.showPerson()
}
//2 参数模板化
template<class T1, class T2>
void doWork2(Person <T1, T2>&p)
{
p.showPerson()
}
//3 整个类 模板化
template<class T>
void doWork3( T &p)
{
p.showPerson()
}
继承时,必须指定出父类中的T数据类型,才能给子类分配内存。
有两种方式
template <class T>
class Base
{
public:
T m_A;
};
//方法1:指定出明确的数据类型
class Son1 : public Base <int>//如果要继承,必须指定出数据类型
{
};
//方法2
//或者 子类也可以不写死数据类型,再指定一个类模板
template <class T1, class T2>
class Son2: public Base<T2>
{
public:
T1 m_B;
}
//类模板的类内声明,类外实现
template <class T1, class T2>
class Person2
{
public:
//类内声明,类外实现。
Person2(T1 name, T2 age);
void showPerson();
T1 m_name;
T2 m_age;
};
template <class T1, class T2>
Person2<T1,T2>::Person2(T1 name, T2 age)
{
this->m_name=name;
this->m_age=age;
}
template <class T1, class T2>
//就算没有用到T1和T2,但是类模板的类外实现就得加上
void Person2<T1,T2>::showPerson()
{
cout<<"姓名"<<this->m_name<<"年龄"<<this->m_age<<endl;
}
存在问题:
类模板的成员函数分文件声明与实现与普通类有个区别,就是在主函数main.cpp中不能 #include <类.h>, 会报错,除非#include <类.cpp>。但是一般别人不会给你cpp源码,
解决方法:
所以一般类模板不会分文件声明与实现,一般声明与实现会写在一个person.h的头文件中,但是正经的.h文件不能有实现,所以后缀会改成person.hpp。 特指类模板的声明与实现才会用到后缀 .hpp。
友元函数分为类内实现和类外实现。
类外实现比较复杂,讨论类内实现
//友元函数类内实现案例
template <class T1, class T2>
class Person
{
public:
//这其实是一个全局函数,只是类内实现的。
friend void printPerson(Person<T1, T2>&p)
{
cout<<m_name<<endl;
}
Person(T1 name, T2 age);
private:
T1 m_name;
T2 m_age;
};
一般情况下,尽量少的去使用类型转换,除非用来解决非常特殊的问题。
使用C风格的强制转换可以把想要的任何东西转换成我们需要的类型。那为什么还需要一个新的C++类型的强制转换呢?
新类型的强制转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换。C++风格的强制转换其他的好处是,它们能更清晰的表明它们要干什么。程序员只要扫一眼这样的代码,就能立即知道一个强制转换的目的。
语法:static_cast<目标类型>(原变量/原对象)
char a='a';
double d = static_cast<double>(a);
//把'a'转为double型,并让d接收。
语法:dynamic_cast<目标类型>(原变量/原对象)
和静态转换差不多,但是比静态转换安全,具有检查的功能。如果发现不安全,就不给你转。
与静态转换的区别:
目前看来,只允许父转子。
语法:const_cast<目标类型>(原变量)
常量转换用来修改对象的底层const属性。
如果对象本身不是一个常量,使用const_cast获得写权利是合法的行为。而如果对象是一个常量,使用const_cast执行写操作就是非法的。
注意:不能将非指针和非引用的变量使用const_cast操作符去移除它的const.
const int * p = NULL;
int *np= const_cast<int *>(p);
//移除const属性
处理异常是一种思想:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。在所有支持异常处理的编程语言中(例如java),要认识到的一个思想:在异常处理过程中,由问题检测代码可以抛出一个对象给问题处理代码,通过这个对象的类型和内容,实际上完成了两个部分的通信,通信的内容是“出现了什么错误”。当然,各种语言对异常的具体实现有着或多或少的区别,但是这个通信的思想是不变的。
一句话总结:异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等)。
C语言的异常处理机制:
c++异常机制相比C语言异常处理的优势?
知识点:
c++异常处理使得异常的引发和异常的处理不必在一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。
异常代码示例
#include
#include
using namespace std;
class MyException//自己定义一个异常
{
public:
void printError()
{
cout<<"我自己的异常"<<endl;
}
};
int myDivision(int a, int b)//该函数作为抛出异常的函数
{
if(b==0)
{
// throw 1;//抛出int类型的异常
// throw 'a';//抛出char类型的异常
// throw MyException();//抛出匿名对象
throw 3.14;//抛出double异常
}
return a/b;
}
void test()
{
int a=10,b=0;
try
{
myDivision(a,b);
}
catch(int)
{
cout<<"整数类型的异常捕获"<<endl;
}
catch(char)
{
cout<<"char类型的异常捕获"<<endl;
}
//抛出的是 throw MyException();
//catch(MyException e)会调用拷贝构造函数,效率会低,
//改成catch(MyException &e)会好一点,具体看2.4
catch(MyException e)//抛出的异常可以是自定义数据类型
{
e.printError();
}
catch(...)//捕获任意类型的异常
{
cout<<"其他类型的捕获"<<endl;
throw;//如果捕获的异常在当前不想处理,可以继续向上跑出,利用throw
}
}
int main() {
try
{
test();
}
catch(...)//下层不想处理的异常,抛给当层做处理
{
cout<<"其他类型异常捕获"<<endl;
}
return 0;
}
从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋
为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类的异常。
一个不抛任何类型异常的函数可声明为:void func() throw()
如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted函数会被调用,该函数默认行为调用terminate函数中断程序。
//异常接口声明
void func() throw(int , double)
{
throw 3.14;
}
直接看案例:该案例也可以看出多态的好处!
需求:提供父类异常类,并在子类中重写virtual void printError()。通过调用父类的同一个虚函数来打印出不同的异常的结果( 同一个函数由于父类指向的子类对象的不同,而打印出不同的内容,这就是多态 )
class BaseException
{
public:
virtual void printError()=0;
};
class NULLPointerException:public BaseException
{
public:
virtual void printError()
{
cout<<"空指针异常"<<endl;
}
};
class OutOfRangeException:public BaseException
{
public:
virtual void printError()
{
cout<<"越界异常"<<endl;
}
};
void dowork()//用来抛异常的函数
{
// throw NULLPointerException();
throw OutOfRangeException();
}
int main()
{
try
{
dowork();
}
catch(BaseException & e)//父类引用指向子类对象
{
e.printError();
}
}
#include
标准库中也提供了很多的异常类,它们是通过类继承组织起来的。异常类继承层级结构图如下:
调用系统异常示例:对类中的年龄做一个判断,若超出限制就抛出系统异常
#include
#include
#include
using namespace std;
//需求:对类中的年龄做一个判断,若超出限制就抛出系统异常
class Person
{
public:
Person(int age)
{
if (age<0 || age>150)
{
throw out_of_range("年龄超出界限");
}
else
{
m_Age=age;
}
}
int m_Age;
};
void test3()
{
try
{
Person p=Person(151);
}
catch(exception &e)//用基类的exception接住就行
{
cout<<e.what()<<endl;
}
}
需求:继承系统提供的父类异常类exception,编写自己的异常类。对类中的年龄做一个判断,若超出限制就抛出自己写的异常。
继承父类异常类exception只需要重写.what()函数就好,但是要重写.what()还是有一些细节要写
//需求:继承系统提供的父类异常类exception,编写自己的类。
class MyOutOfRangeException:public exception
{
public:
MyOutOfRangeException(const char * str)//构造函数
{
//char*可以隐式转化为string,反之不行
this->m_errorInfo= str;
}
MyOutOfRangeException(string str)//重载构造函数
{
this->m_errorInfo= str;
}
virtual const char* what() const//重写.what()函数
{
//将string转为const char *
return m_errorInfo.c_str();
}
string m_errorInfo;
};
class Person1
{
public:
Person1(int age)
{
if (age<0 || age>150)
{
throw MyOutOfRangeException("年龄超出界限");
}
else
{
m_Age=age;
}
}
int m_Age;
};
int main ()
{
try
{
Person1 p1=Person1(151);
}
catch (exception &e)
{
cout<<e.what()<<endl;
}
}
输出结果:
年龄超出界限
#include
智能指针的用途: 帮助管理内存, 可以做到自动释放内存, 避免忘记释放而造成内存泄露.
智能指针的原理:智能指针是在栈上分配的变量, 当智能指针被回收以后, 会将智能指针上管理的内存释放
// 三种初始化方式
1. 直接使用构造函数
shared_ptr<int> ptr(new int(10));
2. 使用make_shared
shared_ptr<int> ptr1 = make_shared<int>(9)
3. 直接赋值
shared_ptr<int> ptr2 = ptr1;
get(): 获取指针值
use_count(): 智能指向的内存的引用计数(有几个智能指针指向这块内存)
reset(): 对智能指针进行重置操作, 使智能指针原有的指向修改为新的指向, 该函数会首先将原有的内存的引用计数减1, 当减小到0的时候就会释放内存.用法: 如ptr1.reset(pnew);