一、RTTI
1、RTTI(Run Time Type Identification):运行时类型识别。通过运行时类型识别,程序能够使用基类的指针或者引用来检查这些指针或者所指的对象的实际派生类型;
2、RTTI我们可以把这个称呼看成是一种系统提供给我们的一种能力,或者一种功能。这种功能或者能力是通过2个运算符来体现的:1)dynamic_cast运算符:能够将基类的指针或者引用安全的转换为派生类的指针或者引用;2)typeid运算符:返回指针或引用所指对象的实际类型;3)要想让RTTI的两个运算符能够正常工作,那么基类中必须至少要有一个虚函数,不然的话这两个运算符工作的结果可能跟我们预测的结果不一样了,因为只有虚函数的存在,这两个运算符才会使用指针或者引用所绑定的对象的动态类型;
3、dynamic_cast运算符:如果该运算符能够转换成功,说明这个指针实际上是要转换到的那个类型,这个运算符能帮我们做安全检查;
4、dynamic_cast运算符:对于引用,如果用dynamic_cast转换失败,则系统会抛出一个std::bad_cast异常;
int main()
{
Human *pHuman = new Men;
Human &q = *pHuman;
try
{
Men &menbm = dynamic_cast<Men &> (q);
cout << "phuman 实际是一个Men类型" << endl;
}
catch(std::bad_cast)
{
cout << "phuman 实际上不是一个Men类型" << endl;
}
delete pHuman;
return 0;
}
5、typeid运算符:1)typeid(类型[指针/引用])、typeid(表达式);返回一个常量对象的引用,这个常量对象是一个标准库type_info(类/类类型)
int main()
{
int status;
Human *pHuman = new Men;
Human &q = *pHuman;
try
{
Men &menbm = dynamic_cast<Men &> (q);
cout << "phuman 实际是一个Men类型" << endl;
}
catch(std::bad_cast)
{
cout << "phuman 实际上不是一个Men类型" << endl;
}
cout << typeid(*pHuman).name()<< endl;// class Men
cout << typeid(q).name()<< endl; // class Men
cout << typeid(123).name()<< endl;//int
cout << typeid("Hello World").name()<< endl;// char [12]
delete pHuman;
return 0;
}
用途:主要是用来判断比较两个指针是否指向同一种类型的对象:1)
void dynamic_cast_use(Human& a)//这种更为安全
{
if(typeid(a) == typeid(Men))
{
Men& b = dynamic_cast<Men&>(a);
b.func();
}
if(typeid(a) == typeid(Women))
{
Women& c = dynamic_cast<Women&>(a);
c.func();
}
}
int main()
{
Human *pHuman = new Men;
Human *pHumanw = new Women;
Human &q = *pHuman;
try
{
Men &menbm = dynamic_cast<Men &>(q);
cout << "phuman 实际是一个Men类型" << endl;
}
catch(std::bad_cast)
{
cout << "phuman 实际上不是一个Men类型" << endl;
}
if(typeid(*pHuman) == typeid(q))
{
cout << "pHuman 和 q 指向同一种类型: " << typeid(q).name() << endl;
}
dynamic_cast_use(*pHuman);//调用Men::func()
dynamic_cast_use(*pHumanw);//调用Women::func();
delete pHuman;
return 0;
}
2)基类必须要有虚函数,否则下边的条件不成立,切记只有当基类有虚函数时,编译器才会对typeid()中的表达式求值,否则如果某个类型不含有虚函数,则typeid()返回的是表达式的静态类型(定义时的类型),既然是定义的类型,编译器就不需要对表达式求值也能知道表达式的静态类型;
int main()
{
Human *pHuman = new Men;
Human *pHumanw = new Women;
if(typeid(*pHuman) == typeid(Men))
{
cout << "pHuman 指向Men类型" << endl;
}
if(typeid(*pHuman) == typeid(Human))
{
cout << "pHuman 指向Human类型" << endl;
}
delete pHuman;
delete pHumanw;
}
6、type_info类:typeid()会返回一个常量对象的引用,这个常量对象是一个标准库类型type_info(类、类类型),具体用法:
1).name:获取类类型名字,返回一个C风格的字符串;
2)==、!=:判断只有两边类类型是否相等;
二、RTTI与虚函数表
1、c++中,如果类里含有虚函数,编译器就会对该类产生一个虚函数表。虚函数表里有很多项,每一项都是一个指针,每个指针指向的是这个类里的各个虚函数的入口地址。虚函数表里,第一个表项很特殊,它指向的不是虚函数的入口地址,它指向的实际上是咱们这个类所关联的type_info对象;
Human *pHuman = new Men;
const type_info &ty = typeid(*pHuman);
pHuman对象里有一个我们看不见的指针,这个指针指向的是这个对象所在的类Men里的虚函数表。
三、基类与派生类关系讨论
1、派生类对象模型简述:
class Human
{
public:
int humanVar;
void HumanFunc();
}
class Men : public Human
{
public:
int menVar;
void MenFunc();//
}
1)一个是含有派生类自己定义的成员变量、成员函数(也就是多个子对象);
2)一个是该派生类所继承的基类的子对象,这个子对象中包含的是基类中定义的成员变量、成员函数(派生类对象含有基类对应的组成部分);
3)以上两个部分数据在内存上不一定是连续存储的;
4)基类指针可以new派生类对象,因为派生类对象含有基类部分,所以我们是可以把派生类对象当成基类对象用的,换一句话说我们可以用基类指针new一个派生类对象;
5)编译器帮助我们做了隐式的这种派生类到基类的转换;
6)这种转换的好处就是有些需要基类引用的地方,我们可以用这个派生类对象的引用来代替,如果有些需要基类指针的地方,我们也可以用派生类指针来代替;
Human *pHuman = new Men;
2、派生类构造函数
1)派生类实际是使用基类的构造函数来初始化它的基类部分。可以说是基类控制基类部分的成员初始化,派生类控制派生类成员的初始化;
2)先执行基类的构造函数,再执行派生类的构造函数;
3)先执行派生类的析构函数,再执行基类的构造函数;
四、即做基类又做派生类
class gra{...}
class fa:public gra{...}//gra是fa的直接基类
class son:public fa{...}//gra是son的间接基类(爷爷类)
1、继承关系一直传递,构成了一种继承链,最终结果就是派生类son会包含它的直接基类的成员以及每个间接基类的成员;
五、不想做基类的类
class A final//表示该类不做基类,若做基类则编译报错
{
...
}
class B final : public C
{
...
}
六、静态类型与动态类型
1、静态类型:变量声明时的类型。静态类型编译的时候是已知的,如下:
Human *pHuman = new Men;
Human &q = *pHuman;
2、动态类型:指的是这个指针/引用所代表的(所表达的)内存中的对象的类型,这里是Men类型。动态类型实在运行的时候才能知道;
3、动态类型、静态类型这种概念只有基类指针/引用的情况下才存在这种静态类型和动态类型不一致的情况。如果不是基类的指针/引用,那么静态类型和动态类型永远都应该是一致的;
七、派生类向基类的隐式类型转换
Human *pHuman = new Men;
Human &q = *pHuman;
1、以上这种2种情况,编译器隐式的帮助我们执行了派生类到基类的转换,这种转换之所以能够成功,是因为每个派生类对象都包含一个基类对象部分,所以基类的引用或者指针可以绑到派生类对象,绑定后指针指向基类;
2、并不存在从基类到派生类的自动转换;
Men *pMen = new Human;//错误
八、父类子类之间的拷贝与赋值
Men men;
Human human(men);//用派生类对象来定义并初始化基类对象,这个会导致基类的拷贝构造函数的执行;
Human human1 = men;//拷贝构造函数;
1、结论:用派生类对象为一个基类独享初始化或者赋值时,只有该派生类的基类部分会被拷贝或者赋值,派生类部分将被忽略掉。
也就是基类只干基类的事,不会做其他事情;
九、左值和右值
1、左值:能用在赋值语句等号左侧的东西,它能够代表一个地址;右值:不能作为左值的值就是右值;c/c++中的一条表达式,要么就是右值,要么就是左值。左值有时候能够被当做右值使用,但其实它是左值,例如 i = i +1;
2、用到左值的运算符有哪些:
1)赋值运算符;
2)取地址&;
3)string、vector、迭代器递、增递减运算符和下标[]需要左值 , 例如:
string abc = "I Love China";
abc[];
vector<int>::iterator iter;
iter++;
iter--;
4)通过看一个运算符在一个字面值上能不能操作,我们就可以判断运算符是否用到的是左值;
5)左值表达式就是左值,右值表达式就是右值;
3、左值:代表一个地址,所以左值表达式的求值结果就得是一个对象,就得有地址,求值结果为对象的表达式,不代表一定是左值。
十、引用分类
1、三种形式的引用:
1)左值引用(绑定到左值);
int value = 10;
int &refVal = value;
2)const引用(常量引用),也是左值引用;
int value = 10;
const int &refVar = value;
3)右值引用(绑定到右值);
int &&refrightValue = 3;//绑定到一个常量上
refrightValue = 5;
2、左值引用:左值引用必须在初始化的时候就绑定左值·,没有空引用的说法;
//const引用不但可以绑定到右值,还可以执行string的隐式类型转换,并将所得到的值放到string临时变量中;
const string &r{"I Love China"};
3、右值引用:就是引用右值,也就是说绑定到右值,&&,希望用右值引用来绑定一些即将被销毁的或者是一些临时的对象上;
string strTest{"I Love China"}
string &&r{strTest};//错误,右值引用不能绑定到左值
string &&r1{"I Love China"};//可以,绑定到一个临时变量,临时变量的内容 I Love China;
4、 总结:返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运算符,都是返回左值表达式的例子;我们可以将一个左值引用绑定到这类表达式的结果上;返回非引用类型的函数,连同算术、关系、位以及后置递增递减运算符,都是返回右值表达式的例子,不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上;
5、重点强调:1)任何函数里边的形参都是左值。void f(int i, int &&w);w是右值引用,但w本身是左值;
2)临时对象都是右值;
3)所有变量,都可以看成左值;
6、右值引用的目的:1)右值引用是c++11引入的新概念。&&代表一种新数据类型,引入新数据类型肯定有目的;
2)提高程序运行效率,把拷贝对象变成移动对象来提高程序运行效率;
3)移动对象如何发生?:&&(应付移动构造函数、移动赋值运算符用的);
十一、std::move函数
1、c++11标准库里的新函数;
2、它的作用只有一个:把一个左值强制转换成一个右值,最终就是一个右值引用可以绑定到左值上去;
int main()
{
int i = 20;
//int &&r = i;//错误,无法将左值绑定到右值引用
int &&r = std::move(i);//把一个左值转换成一个右值,这就是move的能力
r = 10;
cout << " r:" << r << endl; //r = 10
cout << " i:" << i << endl; //i = 10;
i = 30;
cout << " r:" << &r << endl;// &r = 0x0028fe1c
cout << " i:" << &i << endl;// &i = 0x0028fe1c
return 0;
}
3、
int main()
{
string st = "I Love China!";//st[] = "I Love China!", def[] = "";
string def = std::move(st);//st[] = "", def[] = "I Love China!";
//因为std::move()触发了string里的移动构造函数把st的内容转移到了def中去了,而不是std::move()
//做的
string &&def1 = std::move(st);//
}
4、总结:左值是一个持久的值,右值是一个短暂的值,右值引用只能绑定到临时对象,所引用的对象将要销毁或该对象没有其他用户;使用右值引用的代码可以自由的接管所引用对象的内容;
十二、临时对象的概念
int i = 1;
int &&r = i++;//生成临时对象
r = 10;//r = 10, i = 2;
十三、三种产生临时对象的情况和解决方案
1、以传值的方式给函数传递参数 CTempValue:
class CTempValue
{
public:
CTempValue(int a, int b);
virtual ~CTempValue();
CTempValue(const CTempValue &obj);
void Add(CTempValue &obj);//若不加引用则多调用拷贝构造函数,生成临时对象
int value;
int value1;
};
CTempValue::CTempValue(int a, int b)
{
cout << "CTempValue::CTempValue(int a, int b)" << endl;
value = a;
value1 = b;
}
CTempValue::~CTempValue()
{
cout << "CTempValue::~CTempValue()" << endl;
}
CTempValue::CTempValue(const CTempValue &obj)
{
cout << "CTempValue::CTempValue(const CTempValue &obj)" << endl;
}
void CTempValue::Add(CTempValue &obj)
{
obj.value = 10000;
obj.value1 = 10001;
}
int main()
{
CTempValue temp(1, 2);
temp.Add(temp);
cout << "value: " << temp.value << " value1: " << temp.value1 << endl;
return 0;
}
2、1)类型转换生成的临时对象:
class CTempValue
{
public:
CTempValue(int a = 0, int b = 0);
virtual ~CTempValue();
CTempValue(const CTempValue &obj);
CTempValue & operator= (const CTempValue &obj);
void Add(CTempValue &obj);
int value;
int value1;
};
CTempValue::CTempValue(int a, int b)
{
value = a;
value1 = b;
cout << "CTempValue::CTempValue(int a, int b), a " << a << " b "<< b << endl;
}
CTempValue::~CTempValue()
{
cout << "CTempValue::~CTempValue()" << endl;
}
CTempValue::CTempValue(const CTempValue &obj)
:value(obj.value), value1(obj.value1)
{
cout << "CTempValue::CTempValue(const CTempValue &obj)" << endl;
}
CTempValue & CTempValue::operator= (const CTempValue &obj)
{
value = obj.value;
value1 = obj.value1;
cout <<"拷贝赋值"<< "value : " << value << " value1 : " << value1 << endl;
}
int main()
{
CTempValue temp;
temp = 1;//1)用1这个数字创建了一个类型为CTempValue的临时对象
//2)调用拷贝赋值运算符,把这个临时对象里边的各个成员值
// temp对象
//3)销毁这个临时创建的CTempValue对象
return 0;
}
输出:
CTempValue::CTempValue(int a, int b), a 0 b 0
CTempValue::CTempValue(int a, int b), a 1 b 0
拷贝赋值value : 1 value1 : 0
CTempValue::~CTempValue()
CTempValue::~CTempValue()
解决:
int main()
{
CTempValue temp = 1;
return 0;
}
2)隐士类型转换以保证函数调用成功:
int calc1(string &strsource, char ch)//加const
{
const char *p = strsource.c_str();
int icount = 0;
return icount;
}
int main1()
{
char c[100] = {"I Love China!!!, oh yeal!!!"};
int result = calc(c, 'o');//编译报错
return 0;
}
解决:
int calc(const string &strsource, char ch)
{
const char *p = strsource.c_str();
int icount = 0;
return icount;
}
int main()
{
char c[100] = {"I Love China!!!, oh yeal!!!"};
int result = calc(c, 'o');//编译正确
return 0;
}
因为c++语言只会为const引用(const string &strsource)产生临时变量,而不会为非const引用生成临时变量(string &strsource);
3、函数返回对象的时候:
CTempValue Double(CTempValue ts)
{
CTempValue temp;
temp.value = ts.value;
temp.value1 = ts.value1;
return temp;
}
CTempValue Double1(CTempValue &ts)
{
CTempValue temp;
temp.value = ts.value;
temp.value1 = ts.value1;
return temp;
}
CTempValue &Double2(CTempValue &ts)
{
CTempValue temp;
temp.value = ts.value;
temp.value1 = ts.value1;
return temp;
}
int main()
{
CTempValue temp(10, 20);
//1
Double(temp);//实参到形参会调用一次拷贝构造,该函数结束后会立即回收该临时对象
//2
CTempValue temp1 = Double1(temp);//在double中构造的临时对象直接给了temp1,地址一样
//3
CTempValue &&temp2 = Double(temp);//同上
//4
CTempValue &temp3 = Double1(temp);//同上,地址相同
return 0;
}