C++知识点总结(二)

内容比较长,可复制出来进行搜索
如有错误希望大家指出,仅供参考
一、子类的构造、析构、拷贝
1、子类的构造在执行它的构造函数前会根据继承表的顺序执行父类的构造函数。
默认执行父类的无参构造
显示调用有参构造,在子类的构造函数后,初始化列表中显示调用父类的有参构造函数。
2、子类在它的析构执行完后,会根据继承表的顺序,逆顺序执行父类的析构函数。
注意:父类的指针可以指向子类对象,当通过父类指针释放对象时,只会调用父类的析构函数,而这种析构方式有可能造成内存泄漏。
3、当使用子类对象来初始化新的子类对象时,会自动调用子类缺省的拷贝构造函数,并且会先调用父类缺省的拷贝构造函数。
如果子类中实现的拷贝构造,需要显式调用父类拷贝构造,否则就会调用无参构造。
二、私有继承、保护继承
1、私有继承
使用 private 方式继承父类,公开的变成私有,其他的不变(有争议),这种继承方式防止父类的成员扩散。
使用 protected 方式继承父类,公开成员在子类中会变成保护的,其他不变,这种继承方式可以有效防止父类的成员扩散。
子类以私有或保护方式继承父类,会禁止向上造型(子类的指针或引用不能隐式转换成父类的指针或引用,要想实现多态只能以公开方式继承
父类)。
三、多重继承、钻石继承、虚继承
1、多重载继承
在C++中一个子类可以有多个父类,在继承表中按照顺序继承多个父类中的属性和行为,并按照顺序表,调用父类的构造函数。
按照从低到高的地址顺序排序父类,子类中会标记每个父类存储位置。
当子类指针转换成父类的隐式指针时候,编译器会自动计算父类中的内容在子类中的位置,地址会自动进行偏移计算。
2、名字冲突
如果父类中有同名的成员,可以正常继承,但如果直接使用,会造成歧义,需要 类名::成员名 进行访问。
3、钻石继承
假如有一个类A,类B继承类A,类C也继承类A,类D继承B和C。
一个子类继承多个父类,这些父类有一个共同的祖先,这种继承叫钻石继承。
注意:钻石继承不会导致继承错误,但访问祖先类中的成员时每次需要使用 类名::成员名 ,重点是这种继承会造成冗余。
4、虚继承 virtual
当进行钻石继承时,祖先类中的内容会有冗余,而进行虚继承后,在子类中的内容只会保留一份。
注意:但使用虚继承时,子类中会多了一些内容(指向从祖先类继承来的成员)。
class A
{
public:
int a;
};
class B:virtual public A{};
class C:virtual public A{};
class D:public B,public C{};
int main()
{
cout<< sizeof(B)<<endl;
B* p = new B;
p->a = 100;
A* ap = p;
A* ap1 = new C;
ap1->a = 111;
printf("%d %d\n",*(int*)ap,*(int*)ap1);
// printf("%d\n",*((int*)p+1));
// printf("%p %p\n",p,ap);
}
5、构造函数
一旦进行了虚继承祖先类的构造函数只执行一次,由孙子类直接调用,祖先类的有参构造也需要在孙子类中显示调用。
6、拷贝构造
在虚拟继承(钻石)中祖先类拷贝构造也由孙子类直接调用,子类中不再调用祖先类的拷贝构造,在手动实现的拷贝构造时(深拷贝),祖先
类中的内容也由孙子类负责拷贝,同理赋值构造也一样。
四、虚函数、覆盖、多态
1、虚函数
类的成员函数前加 virtual 这种函数就叫做虚函数。
2、覆盖
子类会覆盖父类的虚函数。
3、多态
当子类覆盖了父类的虚函数时,通过父类指针指向子类对象时,调用虚函数,会根据具体的对象是谁来决定执行谁的函数,这就是多态。
class Base
{
public:
virtual void func(void)
{
cout << "我是Base的func函数"<<endl;
}
};
class A:public Base
{
public:
void func(void)
{
cout<<"我是类A的func函数" << endl;
}
};
class B:public Base
{
public:
void func(void)
{
cout<<"我是类B的func函数" << endl;
}
};
class C:public Base
{
public:
void func(void)
{
cout<<"我是类C的func函数" << endl;
}
};
int main()
{
/*覆盖
A* a = new A;
Base* p = a;
Base* b = new Base;
b->func(); // 并没有消失
a->func(); // 调用子类函数
p->func(); // 如果父类的函数是虚函数,调用子类函数
*/
srand(time(NULL));
// 这就是多态
Base* arr[] = {new A,new B,new C};
arr[rand()%3]->func();
}
五、覆盖和多态的条件
1、覆盖的条件
必须是虚函数
必须是父子类之间
函数签名必须相同(参数列表完全一致,const属性也会影响覆盖的结果)
返回值必须是同类型或父子类(子类的返回值要能向父类隐式转换)
访问属性不会影响覆盖
常函数属性也会影响覆盖
class Base
{
public:
virtual Base* func(void)
//virtual void func(void)
{
cout << "我是Base的func函数"<<endl;
}
};
class A:public Base
{
public:
A* func(void)
//在覆盖版本的函数中,所得到的this指针依然是实际对象地址,依然能够调用子类中的函数。
//void func(int num,char* str)
{
cout<<"我是类A的func函数" << endl;
}
};
int main()
{
A* a = new A;
Base* p = a;
p->func();
}
2、重载、隐藏、覆盖(重写)的区别
重载:同一作用域下的同名函数,函数签名不同(类型、个数、顺序、常函数等),构成重载关系。
覆盖:
必须是虚函数
必须是父子类之间
函数签名必须相同(参数列表完全一致,const属性也会影响覆盖的结果)
返回值必须是同类型或父子类(子类的返回值要能向父类隐式转换)
访问属性不会影响覆盖
常函数属性也会影响覆盖
隐藏:父子类之间的同名成员如果没有形成覆盖,且能通过编译,必定构成隐藏。
3、多态的条件
1、父子类之间有的函数有覆盖关系。
2、父类的指针或引用指向子类的对象。
4、在构造、析构函数中调用虚函数
在父类的构造函数中调用虚函数,此时子类还没有创建完成(回顾构造函数的调用过程),因此只能调用父类的虚函数,而不是覆盖版本
的虚函数。
在父类的析构函数中调用虚函数,此时子类已经释放完成,因此只能调用父类的虚函数,而不是覆盖版本的虚函数。
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
class Base
{
public:
Base(void)
{
func();
}
virtual void func(void)
{
cout << "我是Base的func函数"<<endl;
}
~Base(void)
{
func();
}
};
class A:public Base
{
public:
A(void)
{
func();
}
void func(void)
{
cout<<"我是类A的func函数" << endl;
}
~A(void)
{
func();
}
};
int main()
{
A aa;
// A* a = new A;
// Base* p = a;
// p->func();
}
六、纯虚函数和抽象类
1、纯虚函数
在虚函数的声明的后面添加=0,这种虚函数就叫做纯虚函数,可以不实现,但如果实现必须在类外(只能在父类的构造函数、析构函数中调用)。
virtual 返回值 函数名(参数) = 0;
#include <iostream>
#include <stdio.h>
using namespace std;
class Base
{
public:
Base(void)
{
func();
}
// 纯虚函数
virtual void func(void) = 0;
~Base(void)
{
func();
}
};
class A:public Base
{
public:
void func(void)
{
cout << "我是纯虚函数的覆盖"<<endl;
}
};
void Base::func(void)
{
cout<< "我是虚函数" << endl;
}
int main()
{
A a;
a.func();
//Base b;
//b.func();
}
2、抽象类
成员函数中有纯虚函数,这种类叫抽象类,抽象类不能实例化(不能创建对象)。
抽象类必须被继承且纯虚函数被覆盖后,由子类实例化对象。
如果继承抽象类,但没有覆盖纯虚函数,那么子类也将成为抽象类,不能实例化。
3、纯抽象类
所有成员函数都是纯虚函数,这种只能被继承的类叫纯抽象类。
这种类一般用来设计接口,这种类在子类被替换后不需要修改或少量的修改即可继续使用。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void show(void) = 0;
};
class A:public Base
{
public:
void show(void)
{
cout << "我是类A的show函数" << endl;
}
};
class B:public Base
{
public:
void show(void)
{
cout << "我是类B的show函数" << endl;
}
};
class C:public Base
{
public:
void show(void)
{
cout << "我是类C的show函数" << endl;
}
};
enum ClassType{typeA,typeB,typeC};
// 工厂类模式
Base* creat_object(ClassType type)
{
switch(type)
{
case typeA: return new A;
case typeB: return new B;
case typeC: return new C;
default: return NULL;
}
}
int main()
{
Base* p = creat_object(typeA);
p->show();
}

一、虚函数表
什么是虚函数表,在C++的类中,一旦成员函数中有虚函数,这个类中就会多一个虚函数表指针,这个指针指向一个虚函数表,表里面记录了
这个类中所有的虚函数,当这个类被继承,它的子类中也会有一个虚函数表(不管子类中有没有虚函数),如果子类的成员函数中有函数签名与父
类的虚函数一样,就会用子类中的函数替换它在虚函数表中的位置,这样就达到了覆盖的效果。
当通过类指针或引用调用函数时,会根据对象中实际的虚函数表记录来调用函数,这样就达到了多态的效果。
多态类中的虚函数表建立在编译阶段。
二、虚析构
当使用delete释放一个父类指针时,不管实际指向的对象是子类还是父类都只会调用父类的析构函数(多态肯定会出现的问题)。
如果子类的析构函数有需要负责释放的内存,就会造成内存泄漏。
为了解决这个问题,可以把父类的析构函数设置为虚函数,析构函数进行覆盖时不会比较函数名。
当父类的析构函数为虚函数时,通过父类指针或引用释放子类对象时,会自动调用子类的析构函数,子类的析构函数执行完成后也会调用父类
的析构函数。
注意:析构函数可以是虚函数,但构造函数不行
三、强制类型转换
注意:C++中为了兼容C语言,(目标类型)源类型 依然可以继续使用,但C语言的强制类型转换安全性差,因此建议使用C++中的强制类型转换。
注意:C++之父认为如果代码设计的完善,根本不需要用到强制类型转换,而C++的强制类型转换之所以设计的很复杂,是为了让程序员多关注
代码本身的设计,尽量少使用。
C++中的强制类型转换保证没有很大安全隐患。
static_cast<目标类型>(源类型) 编译器会对源类型和目标类型做兼容性检查,不通过则报错。
dynamic_cast<目标类型>(源类型) 编译器会对源类型和目标类是否同为指针或引用,并且存在多态型的继承关系。
const_cast<目标类型>(源类型) 编译器会对源类型和目标类检查,是否同为指针或引用,除了常属性外其他必须完全相同,否则报错。
reinterpret_cast<目标类型>(源类型) 编译器会对源类型和目标类是否为指针或整数进行检查,也就是说把整数转换成指针或把指针转换
为整数。
拓展:
静态编译:指针或引用的目标是确定的,在编译时期就确定了所有的类型检查、函数调用。
动态编译:指针或引用的目标是不确定的(多态),只有在函数调用的时候才确定具体是哪一个子类。
四、I/O流
I/O流的打开模式:
ios::in 以读权限打开文件,不存在则失败,存在不清空
ios::out 以写权限打开文件,不存在则创建,存在则清空
ios::app 打开文件用于追加,不存在则创建,存在不清空
ios::binary 以二进制模式进行读写
ios::ate 打开时定位到文件末尾
ios::trunc 打开文件时清空
fstream/ifstream/ofstream 类用于进行文件操作。
构造函数或成员函数 open 用于打开文件
good成员函数检查流是否可用
eof成员函数用于输入流是否结束
>> 操作符用于从文件中读取数据到变量
<< 操作符用于输出数据到文件
IO流有一系列格式化控制函数,类似:左对齐、右对齐、宽度、填充、小数点位数。
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
fstream fsi("test.txt",ios::in);
//fs.open("test.txt",ios::in);
if(!fsi.good())
{
cout <<"打开失败"<<endl;
}
else
{
cout <<"打开成功"<<endl;
}
string str,s1,s2,s3;
int num = 0;
/* fsi >> str; //读到空格或换行就停止
fsi >> num >> s1 >> s2 >> s3;
cout<<str<<"-"<<num<<"-"<<s1<<"-"<<s2<<"-"<<s3<<endl;
*/
string arr[10];
int i = 0;
while(1)
{
fsi >> arr[i];
if(arr[i].size() == 0)
{
break;
}
i++;
}
for(int j=0; j<i; j++)
{
cout << arr[j] <<"-";
}
fstream fso("test.txt",ios::out);
fso << "hehe" << " " << 100 <<" " <<"adsadsad"<<endl;
}
二进制读写:read/write
read (char_type *__s,streamsize __n)
write (char_type *__s,streamsize __n)
gcount成员函数可以获取上次流的二进制读写操作的字节数。
随机读写:
seekp (off_type,ios_base::seekdir)
功能:设置文件的位置指针。
off_type:偏移值
正值向右,负值向左
seekdir:基础位置
ios::beg 文件开头
ios::cur 当前位置
ios::end 文件末尾
#include <fstream>
int main()
{
fstream fs("test.txt",ios::in);
if(!fs.good())
{
cout << "文件打开失败" << endl;
return -1;
}
// 调整文件的位置指针到末尾
fs.seekp(0,ios::end);
cout << "文件的字节数:" << fs.tellp() << endl;
fs.close();
}
int main(int argc,char* argv[])// 写的有点问题0.0
{
if(argc != 3)
{
cout << "命令错误" << endl;
}
// 读写
fstream fi(argv[1],ios::in);
fstream fo(argv[2],ios::out);
if(!fi.good())
{
cout << "源文件不存在" << endl;
}
cout << "是否要覆盖目标文件,y/n" << endl;
while(1)
{
string a;
cin >> a;
if(a == "y")
{
break;
}
else if(a == "n")
{
return 0;
}
else
{
cout << "指令错误" << endl;
continue;
}
}
while(1)
{
string str;
fi >> str;
if(str.size() == 0)
{
break;
}
fo << str <<" "; // 文件末尾多个空格,需要删除,并且没有换行功能
}
}
五、类型信息 typeid
用于获取数据的类型信息。
name成员函数,可以获取类型的名字,内建类型名字使用缩写。
同时还支持 == != 用来比较是否是同一种类型。
如果用于判断父子类的指针或引用,它不能准确判断出实际的对象类型。但可以判断出具有多态继承关系的父子类的指针或引用,它的实际对象。
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual ~Base(void)
{
}
};
class Test:public Base
{
};
int main()
{
Base b;
Test t;
cout << typeid(b).name() << endl;
cout << (typeid(t) == typeid(b)) << endl;
cout << endl;
Base* p = new Test;
cout << (typeid(*p) == typeid(Test)) << endl;
cout << (typeid(p) == typeid(Test*)) << endl;
}
扩展:
sudo find / -name filename
sudo find / | grep "std"
grep 'Base' * 当前目录查找包含此字符的文件
grep -r 'Base' * 当前目录及所有子级目录,查找包含此字符的文件
grep -r 'Base' * dir 指定目录下及所有子级目录,查找包含此字符的文件
五、异常处理
抛异常
throw 数据
抛异常对象
抛基本类型
注意:不能抛出局部对象的指针或引用(构造函数和析构函数不能抛出异常)。
注意:如果异常没有被捕获处理,程序就会停止。
捕获异常
try{
可以抛出异常的代码
}
catch(类型 变量名) // 根据数据类型进行捕获
{
处理异常,如果无法处理可以继续抛出异常
}
注意:捕获异常的顺序是自上而下的,而不是最精准的匹配,针对子类异常捕获时要放在父类的前面。
函数的异常声明:
返回值类型 函数名(参数列表)throw(类型1,类型2,...)
注意:如果不写异常声明表示什么类型的异常都可能抛出。
注意:如果写了异常声明表示只抛出某些类型的异常,一旦超出异常声明的范围,程序会直接停止,无法捕获。
注意:throw() 表示什么类型都不会抛出
设计异常类:
class Error
{
int errno;
char errmsg[255];
public:
Error(int errno = -1,const char* msg = "未知错误")
{
this->errno = errno;
strcpy(errmsg,msg);
}
int getError(void)
{
return errno;
}
const char* getErrmsg(void)
{
return errmsg;
}
}