文章目录
1. c++关键字
1.1 const_cast、dynamic_cast、reinterpret_cast、static_cast
1.1.1 const_cast
使用const_cast是对const的不变性承诺的一种破坏
const int p1 = 4; // j定义为const.
int *p2 = const_cast(int*)(&p1);// p1理论上是不可改变的,但是通过const_cast可以改变
*p2 = 4; // 虽然通过const_cast将p1改变了,但是这里是未定义行为,较危险
1.1.2 dynamic_cast
dynamic_cast对于指针类型的转换会在运行期提前做检查(会检查class内部的虚函数和表),因此会带来效率上的损失,但它是安全的。dynamic_cast对于指针和引用的转换有着不同的处理方式,当转换指针(即使失败时,也会返回一个空指针)总是成功的返回指针,因此是安全的。当引用转换失败时,则会抛出异常。
1.1.3 reinterpret_cast
reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。
这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个 int* 指针、函数指针或其他类型的指针转换成 string* 类型的指针也是可以的,至于以后用转换后的指针调用 string 类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作:(C++ 标准不允许将函数指针转换成对象指针,但有些编译器,如 Visual Studio 2010,则支持这种转换)。
#include <iostream>
using namespace std;
class A
{
public:
int i;
int j;
A(int n):i(n),j(n) { }
};
int main()
{
A a(100);
int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a
r = 200; //把 a.i 变成了 200
cout << a.i << "," << a.j << endl; // 输出 200,100
int n = 300;
A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
pa->i = 400; // n 变成 400
pa->j = 500; //此条语句不安全,很可能导致程序崩溃
cout << n << endl; // 输出 400
long long la = 0x12345678abcdLL;
pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa
unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u
cout << hex << u << endl; //输出 5678abcd
}
在上面的代码中,如下几行,pa->j = 500; 会导致程序崩溃,原因是n是int型,占4个字节,然而A是类,里面有两个int变量,占8个字节。其中pa->i 会指向n的前四个字节,pa->j 会指向n后四个字节。而n只有4个字节,pa->j的指向超过了n的字节范围,出错。
int n = 300;
A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
pa->i = 400; // n 变成 400
pa->j = 500; //此条语句不安全,很可能导致程序崩溃
cout << n << endl; // 输出 400
如果我们将n的类型改成long long类型(8个字节),如下所示
long long n = 300;
A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
pa->i = 1; // n的前四个字节(以二进制位复制)复制为1(0000 0000 0000 0000 0000 0000 0000 0000)
pa->j = 1; // n的后四个字节(以二进制位复制)复制为1(0000 0000 0000 0000 0000 0000 0000 0000)
cout << n << endl; //
因此最后输出的n为0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0001(二进制), cout输出转换为10进制,因此输出的为4294967297
1.6 static_cast
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。也可以用来将指向父类的指针转换成指向子类的指针。 做这些转换前,你必须确定要转换的数据确实是目标类型的数据,因为static_cast不做运行时的类型检查以保证转换的安全性。也因此,static_cast不如dynamic_cast安全。对含有二义性的指针,dynamic_cast会转换失败,而static_cast却直接且粗暴地进行转换。这是非常危险的。
static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。
#include <iostream>
using namespace std;
class A
{
public:
operator int() { return 1; }
operator char*() { return NULL; }
};
int main()
{
A a;
int n;
char* p = "New Dragon Inn";
n = static_cast <int> (3.14); // n 的值变为 3
n = static_cast <int> (a); //调用 a.operator int,n 的值变为 1
p = static_cast <char*> (a); //调用 a.operator char*,p 的值变为 NULL
n = static_cast <int> (p); //编译错误,static_cast不能将指针转换成整型
p = static_cast <char*> (n); //编译错误,static_cast 不能将整型转换成指针
return 0;
}
指向父类的指针转换成指向子类的指针:
class B {};
class D : public B {};
void f(B* pb, D* pd) {
D* pd2 = static_cast<D*>(pb); // Not safe, D can have fields
// and methods that are not in B.
B* pb2 = static_cast<B*>(pd); // Safe conversion, D always
// contains all of B.
}
上面的第一个类型转换是不安全的。将父类的指针转换为子类指针,比如,一旦使用pd2调用了一个子类D有而父类B没有的方法,则程序就会因越界访问而崩溃(虽然转换了,但是pd2本质上只能包含父类的内容)。第二个转换是安全的,并且这种做法可以使得父类指针去访问子类中的所有内容(如果不转换,原本的父类指针如果指针子类,只能通过多态去访问子类虚函数的内容,如果子类中某些资源在父类中没有定义虚函数,则访问不了)
static_cast和dynamic_cast都可以用于类层次结构中基类和子类之间指针或引用的转换。所不同的是,static_cast仅仅是依靠类型转换语句中提供的信息(尖括号中的类型)来进行转换;而dynamic_cast则会遍历整个类的继承体系进行类型检查。比如:
class B {
public:
virtual void Test(){}
};
class D : public B {};
void f(B* pb) {
D* pd1 = dynamic_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
}
如果pb确实是指向一个D类型的对象,那pd1和pd2的值是相同的,即使pb为NULL。
如果pb实际指向的是一个B类型的对象,那dynamic_cast就会转换失败,并返回NULL(此时pd1为NULL);而static_cast却依据程序员指定的类型简单地返回一个指针指向假定的D类型的对象(此时pd2不为NULL),这当然是错误的。
static_cast还可以在两个类对象之间进行转换,比如把类型为A的对象a,转换为类型为B的对象。如下:
class A;
class B;
A a;
B b;
b = static_cast(a);
此过程可以看做是以a为参数构造一个B类型的临时对象,然后再把这个临时对象赋值给b。如下:
class A;
class B;
A a;
B b;
B c(a);
b = c;
所以,如果让以上代码通过编译,那么B类必须含有以A类的对象(或对象的引用)为参数的构造函数。如下:
B(A& a)
{
// …
}
这实际上是把转换的工作交给构造函数去做了。
1.3 inline
- inline常用于简单的函数展开,即不对函数进行调用,而直接返回函数的本体,例如下面的例子会直接将函数展开为 return a+b,因为函数的调用会涉及到进栈、出栈以及资源的调用,简单的函数这样做无疑是浪费资源的。但是即使是使用了inline,编译器也不一定会展开,也不意味着使用inline就会提高运行速度,这取决于编译器
inline add(int a, int b) // 可将函数展开为return a+b
{
return a+b;
}
- inline与类
类内定义类内声明都是内联函数。如果在类内定义,类外声明,此时加了inline才是内联函数,没加inline就不是内联函数。
1.4 nullptr
nullptr专门指空指针,在c++中实际上是一个类型std::nullptr_t
template<typename T, typename U>
void func(T t, U u)
{
t(u);
}
void nullpointer(int* a){
std::cout<<"hell";
}
func(nullpointer, nullptr); //成功, nullptr是nullptr_t类型, 因此模板函数中U被推导为nullptr_t类型。func中再去调用函数nullpointer
func(nullpointer, 0); // 失败,在模板函数func中, 0 会被推导为int类型,因此无法调用函数。可以手动将参数进行强转,func(nullpointer, (int*)0),这样就可以调用成功;
func(nullpointer, NULL);// 失败,NULL无类型,因为模板函数无法推导其类型
1.5 explicit 显示转换
按照默认规定,只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象,如下面所示:
class String {
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
String s1 = “hello”; //OK 隐式转换,等价于String s1 = String(“hello”);
但是有的时候可能会不需要这种隐式转换,如下:
class String {
String ( int n ); //本意是预先分配n个字节给字符串
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
下面两种写法比较正常:
String s2 ( 10 ); //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串
下面两种写法就比较疑惑了:
String s4 = 10; //编译通过,也是分配10个字节的空字符串
String s5 = ‘a’; //编译通过,分配int(‘a’)个字节的空字符串
s4 和s5 分别把一个int型和char型,隐式转换成了分配若干字节的空字符串,容易令人误解。
为了避免这种错误的发生,我们可以声明显示的转换,使用explicit 关键字:
class String {
explicit String ( int n ); //本意是预先分配n个字节给字符串
String ( const char* p ); // 用C风格的字符串p作为初始化值
//…
}
加上explicit,就抑制了String ( int n )的隐式转换,
下面两种写法仍然正确:
String s2 ( 10 ); //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串
下面两种写法就不允许了:
String s4 = 10; //编译不通过,不允许隐式的转换
String s5 = ‘a’; //编译不通过,不允许隐式的转换
因此,某些时候,explicit 可以有效得防止构造函数的隐式转换带来的错误或者误解
explicit 只对构造函数起作用,用来抑制隐式转换。如:
class A {
A(int a);
};
int Function(A a);
当调用 Function(2) 的时候,2 会隐式转换为 A 类型。这种情况常常不是程序员想要的结果,所以,要避免之,就可以这样写:
class A {
explicit A(int a);
};
int Function(A a);
这样,当调用 Function(2) 的时候,编译器会给出错误信息(除非 Function 有个以 int 为参数的重载形式),这就避免了在程序员毫不知情的情况下出现错误。
总结:explicit 只对构造函数起作用,用来抑制隐式转换。
1.6 volatile 关键字
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统,硬件或者其他线程等。
遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
(1)多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量==被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,==这会造成程序的错误执行。==volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,==如下:
volatile BOOL bStop = FALSE;
// 线程1:
while( !bStop ) { ... }
bStop = FALSE;
return;
//线程2中,要终止上面的线程循环:
bStop = TRUE;
while( bStop ); //等待上面的线程终止,
在线程1中,刚开始的标志 bStop = FALSE, 并且bFlag在线程1中的while前面语句中并没有改变,因此bStop会被保存在寄存器中,while( !bStop )会直接取寄存器中的值,因此线程1会一直在while死循环中;
在线程二中,对bStop进行了修改,如果bStop没有加 volatile 关键字进行修饰,那么即使在线程二中进行了修改,线程一仍然读寄存器中的bStop值,因此仍然会死循环。而加了volatile关键字后,就会告诉编译器,每次在while循环中读取bStop,要在内存中读取,而不是在寄存器中。内存中的bStop会被线程二改变,因此此时就打破了线程1的死循环。
1.7 lambda表达式
lambda表达式一般形式为:
[捕获列表](参数列表)->返回类型
{
函数体内容
}
注意:
(1)lambda表达式可以有默认参数值
auto f = [](int a=8)->int{ return a+1};
cout<<f()<<endl;
1.7.1 lambda捕获列表
(1)[ ] 不捕获任何参数
int a = 10;
auto f = []{return a;}; // 错误,未使用捕获参数,lambda表达式内部不可以使用a
(2)[&] 捕获外部所有参数,并在函数体内部引用使用
int a = 10;
auto f = [&]
{ a = a + 1;
return a;
};
cout << f()<<endl; // 输出11
cout<<a<<endl; // 输出11
(3) [=], 捕获外部作用域所有变量,但是在lambda内部只能读,不能对其进行修改
int a = 10;
auto f = [=]
{ a = a + 1; // 错误,不能修改
return a;
};
cout << f()<<endl; // 输出11
cout<<a<<endl; // 输出11
(4)[this] 一般用于类中,捕获类本身,在类中也可以使用[=] 、[&], 同样可以达到[this]的效果。==但是 [=] 只能读类中的变量不能修改,要是想修改类中的变量,要用[&] 和 [this] ==
class Test
{
public:
int m = 10;
void test()
{
auto f = [&] {
m = m + 1;
return m;
};
cout << f() << endl;
}
};
(5) [变量名] 、[&变量名],只捕获某一根变量,不捕获其他变量,加入&可以修改该变量,未加&不可以改变
int a = 10;
auto f = [&a] {
a = a + 1; // 加入&可以修改变量
return a; };
auto f = [a] {
a = a + 1; // 错误,未加&,不可以修改变量
return a; };
cout << f() << endl;
1.7.2 lambda实现递归
通常lambda是不能实现递归的,原因是,由于lambda表达式的匿名特性,无法直接在lambda内部递归调用lambda,我们需要另寻其道来解决该问题。因此需要借助 std::function 来封装一个lambda表达式(当然也可以封装其他的函数)来实现lambda递归。std::function 模板类的使用可以参见
链接: std::function.
实现 1 + 2 + … + n;
const auto& sum = [](const int& n) // const int &就是只读,但是由于使用了引用,效率更高,不用值传递了,并且加了const更加安全
{
// 相当于在lambda内部构造了一个显示的递归函数 s,
std::function<int(const int&)> s = [&](const int &n){
return n == 1 ? 1 : n + s(n - 1);
}
return s(n);
}
cout << sum(10) <<endl;
更多的lambda实现递归,可以参考lambda实现递归
1.8 extern关键字
2.class类
2.1 friend
friend可以访问私有成员。
class STU
{
private:
int a;
void fun() // 私有函数,类外不可访问
{
a = 100;
}
friend void test01(); // 友元函数,类外可以访问类内私有成员
friend class TEACH; // 友元类
};
void test01()
{
STU s;
s.fun(); // test01是友元,因此可以在类外访问
}
class TEACH{
public:
STU s;
s.fun(); // 友元类可以访问类内成员
};
2.2 构造函数
2.2.1 默认构造函数:
1.作用:初始化类内变量
2.执行时间:创建对象时自动调用构造函数
class STU
{
int a;
STU() // 默认构造函数
{
a = 10;
}
private:
// int a =10 ;// 出错,普通的成员变量在类内初始化会出错。只有 静态常量整型数据成员 才可以在类中初始化
};
int main(){
STU st; // 初始化对象时构造函数自动执行
STU* st; // 创建类指针时,构造函数不会自动执行,只有为指针new空间时才会执行构造函数
STU* st1 = new STU; // new会为类指针分配堆区内存空间,执行构造函数
}
2.2.2 有参构造函数
class STU
{
int a;
int b;
STU(int value1, int value2 = 10) // 有参构造函数,其形参可以有默认值
{
a = value1;
b = value2;
}
private:
// int a =10 ;// 出错,普通的成员变量在类内初始化会出错。只有 静态常量整型数据成员 才可以在类中初始化
};
int main(){
STU st(10); //有参构造函数的调用
STU st1(10, 20);
STU* st2 = new STU(10,20); // 类指针的初始化
}
2.2.3 拷贝构造函数
拷贝构造函数功能:逐个复制非静态成员的值(默认构造函数是浅拷贝),复制的是成员的值。
class CStu
{
public:
CStu()
{
}
CStu(const CStu& a) // 拷贝构造函数
{
}
};
CStu fun()
{
CStu a;
return a;
}
}
拷贝构造函数的调用时机:
1. 创建一个对象,并将其初始化为同类现有对象。主要是以下几种情况
CStu st1; // 调用默认构造函数
CStu stNew(st1);// 调用拷贝构造函数
CStu stNew = st1;// 调用拷贝构造函数
CStu stNew = CStu(st1); //调用拷贝构造函数
CStu* stNew = new CStu(st1);// 调用拷贝构造函数
2. 当程序生成对象副本时,即函数参数传递对象的值或者函数返回对象,也会调用拷贝构造函数,如以下所示
void fun(CStu a) // 函数参数为类对象,调用拷贝构造函数
{
}
CStu fun()
{
CStu a;
return a; // 函数返回值为类对象,调用拷贝构造函数
}
2.2.4 构造函数的调用(括号法,显示法,隐式转换法)
假设有一个类Person
括号法:
Person p1; // 调用默认构造函数
Person p2(10); // 调用有参构造函数
Person p3(p2); // 调用拷贝构造函数
显示法:
Person p1; // 调用默认构造函数
Person p2 = Person(10); // 调用有参构造函数
Person p3 = Person(p2); // 调用拷贝构造函数
隐式法:
Person p4 = 10 // 相等于 Person p4 = Person(10) // 调用有参构造函数
Person p5 = p4; // 拷贝构造函数
2.2.3 深拷贝
默认拷贝构造函数会存在内存重复释放的问题,此时要定义深拷贝构造函数
class CStu
{
public:
int *a;
CStu()
{
a = new int[2];
a[0] = 12;
a[1] = 13; // 12/0
}
CStu(const CStu& b) // 深拷贝
{
//this->a = b.a;
//申请空间
this->a = new int[2];
this->a[0] = b.a[0];
this->a[1] = b.a[1];
//memcpy(this->a, b.a, 8); //memory copy
}
~CStu()
{
delete[] a;
}
};
int main()
{
{
CStu at;
cout << at.a[0] << " " << at.a[1] << endl;
fun(&at);
CStu st = at;
cout << st.a[0] << " " << st.a[1] << endl;
}
system("pause");
return 0;
}
2.2.5 运算符重载
2.2.5.1 +号运算符重载
class CStu
{
public:
int nAge;
double dScore;
// 类内 +号运算符重载
int operator+(int a) //this 类+a
{
return (this->nAge + a);
}
CStu()
{
nAge = 12;
dScore = 12.12;
}
};
// +号运算符重载 类外运算符重载
int operator+(CStu& st, int a) // 引用,不用值传递,否则会调用拷贝构造
{
return (st.nAge + a)
}
int main()
{
int a = 13;
a + 13;//
CStu st1; //对象1
st1 + 12; // 调用 +号 运算符重载
}
2.2.5.2 左移运算符<< 重载
重点注意左移(右移)运算符重载,类内和类外是有区别的。要实现直接输出类,成员重载<<符号无法实现,只能实现p<<cout这种形式,而无法实现cout<<p
#include<iostream>
using namespace std;
class Person1
{
public:
//要实现直接输出类,成员重载<<符号无法实现,只能实现p<<cout这种形式,而无法实现cout<<p
ostream& operator<<(ostream &cout)//这种是函数引用
{
cout << m_A << "和" << m_B;
return cout;
}
int m_A;
int m_B;
};
//只能利用全局函数重载<<来实现cout<<p;
//ostream& operator<<(ostream & a, Person1 p)
//{
// a << p.m_A << "和" << p.m_B;
// return a;
//}
void test02() {
Person1 p;
p.m_A = 10;
p.m_B = 20;
//operator<<(cout,p);
//cout << p; //利用全局函数来重载,等价于operator<<(cout,p);
//p << cout;//利用成员函数重载来实现, 等价于p.operator<<(cout);只能实现p<<cout这种形式,而无法实现cout<<p
p.operator<<(cout);
}
int main() {
test02();
return 0;
}
2.2.5.3 特殊运算符重载
(), [],=, ->这些运算符必须是成员函数重载,不可以是全局函数重载
2.2.6 类的继承
2.2.6.1 public继承
在C++的继承中,子类会继承父类中除构造函数和析构函数之外的所有成员(正所谓儿子无法继承父亲的生死) 。而公有继承(public)就相当于先将从父类那里继承的全部成员放到子类的public部分
class CPeople //基类 父类
{
public:
void Study()
{
cout << "Study" << endl;
}
};
class CChild : public CPeople //派生类 子类
{
public:
void GoToSchool()
{
Study();
cout << "GoToSchool" << endl;
}
};
int main()
{
CChild child; // 继承了基类CPeople
child.Study(); // 派生类可以调用父类中的函数
}
2.2.6.2 继承的范围
public: 能被类成员函数、子类函数、友元访问,也能被类的对象访问。
protected: 只能被类成员函数、子类函数(public、protected继承时的子类函数中可以访问)及友元访问。不能被其他任何访问,本身的类对象也不行。
private: 只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
2.2.6.3 继承的构造函数顺序
-
继承的无参(默认)构造函数顺序为:先父后子
-
继承的有参构造函数顺序依然为:先父后子,但是继承的使用方法不一样,如下所示
父类中存在有参构造函数是,子类必须要有构造函数(不能是系统默认的,必须是自己手写的),并且在子类中利用参数列表的形式标记出父类的有参构造函数参数。
#include <iostream>
using namespace std;
class cgrandfather
{
public:
//cgrandfather(int a, int b)
//{
//}
cgrandfather(int c)
{
}
};
class cfather : public cgrandfather
{
public:
cfather(int a) : cgrandfather(a) // 在基类的构造函数中标出父类的有参构造函数参数
{
cout << "i am fatherclass\n";
}
void show()
{
cout << "hello\n";
}
};
class cson : public cfather
{
public:
int b;
cson(int a) : cfather(a) // 在基类的构造函数中标出父类的有参构造函数参数
{
}
void show()
{
cout << "hello11\n";
}
};
int main()
{
cson son(3);
son.cfather::show();
system("pause");
return 0;
}
2.2.6.3 继承的析构函数顺序
析构函数的执行顺序和构造函数正好相反,辈分小(子类)的先执行,辈分大的后执行
2.2.6.4 继承中同名函数处理方式(覆盖)
在继承中,当子类的成员函数(成员变量)与父类中的成员函数一样时,在调用时会调用子类的成员函数,即对父类进行覆盖,如果想要调用父类的函数,要加父类的作用域。
注意,只要父类和子类函数名一样时(无论参数是否不同),子类会将父类的覆盖,而不会产生重载,要想调用父类的函数,一定要加作用域
#include <iostream>
using namespace std;
class CFather
{
private:
int b;
public:
int a;
CFather()
{
a = 12;
b = 13;
}
void fun(int a)
{
cout << "Cfather Fun" << endl;
}
friend void show();
};
class CSon : public CFather
{
private :
int c;
public:
int a;
CSon()
{
a = 10;
c = 14;
}
void fun() //名字相同 就覆盖
{
cout << "CSon Fun" << endl;
}
};
int main()
{
CSon so;
so.fun(); // 调用子类的fun()函数
so.CFather::fun(1); // 加上作用域后,调用父类的fun()函数
system("pause");
return 0;
}
2.2.7. 多态
动态多态满足条件
- 有继承关系
- 子类重写父类的虚函数
#include<iostream>
using namespace std;
class Animal
{
public:
//void spqak()//地址早绑定,要想在编译时确定运行哪个子类函数,需要使用虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
//重写与重载不同,重写需要函数名 参数列表都完全相同
void speak()//子类重写也可以加上virtual
//virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
//动态多态满足条件
//1. 有继承关系
//2. 子类重写父类的虚函数
void doSpeak(Animal& animal)//动态多态的使用 父类的指针或引用 指向子类对象
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat); // 如果父类没有虚函数,则调用父类的speak()函数。如果有virtual,则调用子类的
}
int main1() {
test01();
return 0;
}
2.2.7.1 虚函数
如果父类中的同名函数加了virtual,则变为虚函数,此时则调用派生类的同名函数。并且派生类的同名函数也会变为虚函数。派生类中的同名函数virtual可以不加。但是父类的同名函数必须要写。
#include <iostream>
using namespace std;
class CFather
{
public:
virtual void Show()
{
cout << "class CFather\n";
}
};
class CSon : public CFather
{
public:
int aa;
void Show()
{
cout << "class CSon\n";
}
};
class CSon1 : public CFather
{
public:
int aa;
void Show()
{
cout << "class CSon1\n";
}
};
int main()
{
CFather* fa = new CSon1; // 多态
fa->Show(); //普通的只能调用属于父类的成员。如果父类中的同名函数加了virtual,则变为虚函数,此时则调用派生类的同名函数。并且派生类的同名函数也会变为虚函数。派生类中的同名函数virtual可以不加。但是父类的同名函数必须要写。
system("pause");
return 0;
}
2.2.7.2 虚表指针和虚函数表
重点看以下网页链接
C++ 虚函数表剖析 - 知乎
https://zhuanlan.zhihu.com/p/75172640
2.2.7.3 重载、重写、隐藏
重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重载和重写的区别:
(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
(3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
隐藏和重写,重载的区别:
(1)与重载范围不同:隐藏函数和被隐藏函数在不同类中。
(2)参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。
2.2.7.4 纯虚函数和抽象类
抽象类: 只要有一个纯虚函数(virtual void func() = 0; // 纯虚函数),这个类称为抽象类
抽象类特点:
- 无法实例化对象
- 抽象类的子类必须重写父类中的纯虚函数,否则子类也是抽象类
#include<iostream>
using namespace std;
#include<string>
class Base
{
public:
//纯虚函数 只要有一个纯虚函数,这个类称为抽象类
//抽象类特点。1.无法实例化对象
//2. 抽象类的子类必须重写父类中的纯虚函数,否则子类也是抽象类
virtual void func() = 0; // 纯虚函数
};
class Son :public Base
{
void func()
{
cout << "子类函数重写" << endl;
}
};
void test05()
{
Base* base = new Son;
base->func();
delete base;
}
int main3() {
test05();
return 0;
}
2.2.7.5 虚析构和纯虚析构
多态使用时,如果子类有属性开辟到堆区,那么父类指针在delete时无法调用子类的析构函数,会造成子类的内存泄露。
解决方法: 将父类的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共同点:
- 可以解决父类指针释放子类对象
- 都需要具体的函数实现(纯虚析构类内声明,类外定义)
不同点:
如果是纯虚析构,该类是抽象类,无法实例化对象。
#include<iostream>
using namespace std;
#include<string>
class Animal1
{
public:
virtual void speak() = 0;
Animal1()
{
cout << "Animal1 的构造函数" << endl;
}
//virtual ~Animal1()//虚析构
//{
// cout << "Animal1 的析构函数" << endl;
//}
virtual ~Animal1() = 0;//纯虚析构函数,类内声明,类外定义
};
Animal1::~Animal1()
{
cout << "Animal1 的纯虚析构函数" << endl;
}
class Dog :public Animal1
{
public:
void speak()
{
cout << *m_Name << "Dog在说话" << endl;
}
Dog(string name)
{
cout <<"Dog 的构造函数" << endl;
m_Name= new string(name);//构造函数中创建堆区数据,析构函数中手动释放
}
~Dog()
{
if (m_Name!=NULL)
{
cout << "Dog 的析构函数" << endl;
delete m_Name;
m_Name = NULL;
}
}
string *m_Name;
};
void test07()
{
Animal1 *ani=new Dog("Tom");//new完之后一定要记得手动删除
ani->speak();
delete ani;//如果不用虚析构或者纯虚析构函数,则父类指向的子类堆区数据无法释放
}
int main5() {
test07();
return 0;
}
2.3 其他
2.3.1 初始化列表
无论初始化列表怎么初始化,其成员初始化的顺序一定是自上向下的,即一定是先初始化 a, 再初始化 b。
多个构造函数,初始化列表绑定所在的构造函数
class STU
{
int a;
int b;
STU(): a(10), b(20) // 利用初始化列表给类成员赋初值
{
}
STU(int value1, int value2): a(value1), b(value2) // 利用初始化列表给类成员赋初值
{
}
}
2.3.2 临时对象(匿名对象)
注意:不要利用拷贝构造函数初始化匿名对象,即
STU s(10,20);
STU s1 = s; // 拷贝构造
STU(s1); 这个不是临时对象,编译器会认为重定义。这行等价于 STU (s1) = STU s1; 这与上面的重定义了
class STU
{
int a;
int b;
STU(): a(10), b(20) // 利用初始化列表给类成员赋初值
{
}
STU(int value1, int value2): a(value1), b(value2) // 利用初始化列表给类成员赋初值
{
}
}
int main()
{
STU(); //创建临时对象,作用域只在该行
STU(10, 20);// 创建临时对象
}
函数返回一个对象值时,会产生临时对象,函数中的返回值会以值拷贝的形式拷贝到被调函数栈中的一个临时对象。
如:
Integer Func()
{
Integer itgr;
return itgr;
}
void main()
{
Integer in;
in = Func();
}
表达式 Func() 处创建了一个临时对象,用来存储Func() 函数中返回的对象,临时对象由 Func() 中返回的 itgr 对象拷贝构造(值传递),临时对象赋值给 in后,赋值表达式结束,临时对象被析构。
2.3.3 malloc、free和new、delete区别
malloc、free和new、delete最本质的区别是在申请对象(class)空间时:new、delete会分别触发构造函数和析构函数,而malloc、free则不会。
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
更多可见
整理:new/delete和malloc/free的区别和联系_一苇以航-CSDN博客
new和malloc区别
c++ new和malloc的区别_Arcobaleno-CSDN博客_c++ new和malloc
new和malloc区别2
注意
c++中new一个对象数组时
- 当这个对象有析构函数时,会多出四字节。如下所示,class是一个空白类,里面没有任何变量,因此sizeof(A) = 1。 然而,当我们new一个对象数组后,比如A* p1 = new A[3];此时理论上会开辟3个字节,来依次保存这个类数组,然而实际上却开辟了7个字节。这是为什么呢?
#include <iostream>
class A
{
public:
A()
{
std::cout << "call A constructor" << std::endl;
}
/*~A()
{
std::cout << "call A destructor" << std::endl;
}*/
void* operator new(size_t size)
{
std::cout << "call A::operator new[] size:" << size << std::endl;
return malloc(size);
}
void operator delete[](void* p)
{
std::cout << "call A::operator delete[]" << std::endl;
free(p);
}
void operator delete(void* p)
{
free(p);
}
};
void* operator new[](size_t size)
{
std::cout << "call global new[] size: " << size << std::endl;
return malloc(size);
}
void operator delete[](void* p)
{
std::cout << "call global delete[] " << std::endl;
}
int main(int argc, char* argv[])
{
std::cout << "sizeof A " << sizeof(A) << std::endl;
A* p1 = new A[3];
delete[]p1;
system("pause");
return 0;
}
这是因为在申请数组空间时,当在delete释放数组空间时,delete并不知道数组长度是多少。这个职责由new[]完成。标准库中new[]的实现一般是先申请一块sizeof(T) * n + x的空间,使用最初的空间记录数组长度,从下一个对齐了的地址开始才是对象数组实际使用的空间。这点可以以下通过简单修改程序观测到。构造函数中打印this指针,new[]函数中先记录malloc的值,打印并返回。即
结合上面那段new[]应该负责记录数组长度这一规定,可以得出如下规则。
- 由new[]返回的空间中最初空间用于长度记录,答主实测VC上使用的是4字节int。
- 依据内存对齐要求补齐空间。
- 对象数组实际占用的空间。
2.当这个对象没有析构函数时
详情见
c++为什么定义了析构函数的类的operator new[]传入的参数会多4字节?
link
2.3.4 this指针
this指针是指向当前对象的指针,只有当对象创建成功后,this指针才存在。this不是成员,是类成员函数的隐含参数,这意味着this指针只能在类成员函数内部使用,在类外部或者类成员函数外部使用都不行。
class CStu
{
public:
int a;
this->a;
CStu(int a)
{
this->a = a; //CStu*
this->Show();
}
this->show();// 出错。this指针只能在类成员函数内部使用,在类外部或者类成员函数外部使用都不行。
void Show() //this不是成员,是类成员函数的隐含参数。相当于void Show(this)
{
cout << a << endl;
this->a;
}
CStu* GetAddr()
{
return this; ///得到的是对象的地址。*this,
}
CStu GetAddr1()
{
return *this; ///得到的是对象本身
}
};
2.3.5 常函数
常函数:
形式: void fun() const {}
构造函数和析构函数不可以是常函数
特点:①可以使用数据成员,不能进行修改,对函数的功能有更明确的限定;
②常对象只能调用常函数,不能调用普通函数;
③常函数的this指针是const CStu*.
class CStu
{
public:
static int a;
CStu()
{
a = 12;
}
~CStu() //CStu*
{
}
//int a;
void SHow () const //const CStu*
{
//a = 13; //常函数 不能修改数据成员
int b = 2; //内部定义的变量是允许修改的
b = a*b;
//this->a = 12;
cout << b << "I am show()" << endl;
}
void fun()
{
cout << "i am fun" << endl;
}
}
int main()
{
const CStu st; //常对象
st.SHow(); // 常对象只能调用常函数,不能调用普通函数
//st.fun();//出错,常对象不能调用普通函数
system("pause");
return 0;
}
2.3.6 静态成员
静态成员特点:
1. 静态成员变量类内声明,类外初始化
2. 静态成员(函数,变量)是类本身的属性,属于对象共享(即使创建多个对象,静态成员也只有一份)。静态成员函数无this指针,this指针要在类初始化后才存在,而静态成员函数是类的属性,创建完类就存在,因此无this指针。
3. 静态成员函数只有使用静态成员变量,不能使用普通成员变量.
静态成员属于所有对象共享,它不在某个对象的空间里,如下,此时CStu类的大小 sizeof(CStu) = 1,即使里面有了一个静态成员,但由于静态成员不属于某一个单独的对象,因此此时相当于空类,因此其大小为1.
class CStu
{
static int a; //类外初始化
}
class CStu
{
private:
//static int a; //类外初始化
public:
//static const int a = 13; //只有静态常量整型成员才能初始化时直接赋值
// int a = 10; //不可以定义时就赋值
static int b; // 静态成员类内定义
int a;
static void fun()
{
cout << "i am static" << endl;
a = 12; // 出错,非法引用。静态函数不能调用普通成员变量
b = 12;
}
CStu()//:a(2)
{
//a = 12;
}
};
int CStu::b = 13; // 静态成员类外初始化
int main()
{
//类名作用域
cout << cstu::b << endl; //调用静态成员变量
cstu::fun();// 调用静态成员函数,可以不用初始化就直接调用静态成员函数
//对象
cstu st;
cout << st.b << endl;
st.fun();
system("pause");
return 0;
}
2.3.7 左值引用和右值引用以及move
什么是引用?引用就是给一个变量取别名,它必须指向一个确定的变量
int a = 10;
int& ref = a; // 引用
c++ 左值引用与右值引用 - 知乎
https://zhuanlan.zhihu.com/p/97128024
右值引用:
右值引用最大的作用是可以避免拷贝构造函数的调用,而使用移动构造函数,拷贝构造函数可以分为深拷贝和浅拷贝,深拷贝会开辟堆栈和内存,然后再进行复制,移动构造则避免了这些,带来效率上的提升。
move
上面的右值引用可以使用移动语义避免拷贝构造,带来了效率上的提升,但是只能是右值,有没有一种办法可以让左值也可以使用移动语义避免拷贝构造呢?答案就是move,move可以让左值转换为右值,从而使用移动语义。例如下面
vector<int> v1{1,2,3,4};
vector<int> v2 = v1; //调用拷贝构造,效率较低
vector<int> v3 = move(v1); // 调用移动构造
再例如下面的swap函数模板,使用了常见的复制
template<typename T>
void swap(T &a, T &b)
{
T tmp = a; //复制
a=b; // 复制
b = tmp; // 复制
}
上面使用了三次复制(虽然是值复制),效率较低,使用move可以提升效率
template<typename T>
void swap(T &a, T &b)
{
T tmp = mpve(a); //复制
a= move(b); // 复制
b = move(tmp); // 复制
}
更多可见
https://blog.csdn.net/bureau123/article/details/112696446
重点可看链接
3. c++ STL
3.1 SGI STL 中的空间分配器
在传统的malloc申请内存时,由于malloc申请的内存中还需要划分出一部分来交由内存管理,例如通常会留出8字节来记录该段内存大小等等,这就会导致有部分的内存用户无法用上,因为这部分是交给内存进行管理的,这部分就会造成浪费。
在STL中,每个容器申请的类型都是一样的(例如申请vector < int > 时,里面全是int类型),如果这里只是简单的调用malloc就会出现很大的浪费,因为malloc每申请一个int,就会留有一部分内存去记录这个大小(通常8字节),如果我们申请100万个int,那么malloc就会留有800万个字节去记录这个大小,这显然是十分浪费的。
在STL中采用的是一级分配器和二级分配器的策略。二级分配器就是一个内存池,池中有十六条自由链表,当申请的内存小于128字节时,就会由二级分配器去申请内存;如果超过128字节,就会由一级分配器去申请内存,一级分配器就是简单的封装了malloc。
具体看:
链接1: link.
链接2: link.
3.2 STL 中的allocator
详细的参考:C++STL - 容器空间配置器allocator的原理
空间配置器的核心功能就是把对象的内存开辟和对象构造的过程分解开,对象析构和内存释放的过程分解开.
为什么要将开辟内存和对象构造分开进行,析构和内存释放分开进行呢?
简单地说就是避免资源浪费。假如有个对象A,我们利用vector< A > a(10) 来申请10个A,如果对象的构造和内存申请不分开,那么在创建完vector之后不仅会分配10个A的内存空间,还会执行10次A的构造函数。但这会造成效率上的低下,试想,加入我们虽然申请了10个A内存空间,但是最后实际上用到的A对象只有3个,也就是说我们申请了10个A,但是还有7个实际上没用到,那么这7个A的构造函数执行是多余的,因为我们并不会用到它们。 因此,STL中的allocator在申请空间时,会将构造函数和内存申请分开进行,也就是说,当我们在执行vector< A > a(10) 时, 系统会为我们开辟10个A的内存,但是不会立刻去调用10次A的构造函数,而是我们在实际上使用的A的时候,才会取调用A的构造函数取实例化一个对象。
同理,当程序执行完进行析构时,也没有必要申请10个A空间,就调用10次构造函数,而是我们实际上使用了几次A,就调用几次A的析构函数,但是内存释放仍然是将申请的10个A空间全部释放。
3.2 萃取技术
类型萃取帮助我们提取出自定义类型进行深拷贝,而内置类型统一进行浅拷贝,也就是所谓的值拷贝。
帮助我们挑选某个对象的类型,筛选特定的对象来做特定的事。
链接: link.
3.1 vector
vector的底层实现一段连续的线性内存空间。vector具有连续的地址存储空间。如下图所示,通常需要三根指针即可判断其位置,start,end, end_of_storage。并且其容量(capacity)通常大于其尺寸(size)。当vector插入元素时,如果原来的容量不够,则会扩容(有些编译器会扩容2倍,有些则不一定)。具体的扩容方式为:将原来的所有元素复制到另一块连续存储的空间,然后再在新的存储空间插入新元素,这样就保证元素的地址空间是连续的。如果扩容后的内存空间不够了,则结束此次vector。
注意: 1. 数组的元素是不能删的,只能覆盖。
2. 数组下标都是从0开始的。
3. 数组内存空间的地址是连续的
4. vector数组是单端的,只能进行push_back或者pop_back。如果想要在其他位置操作,要用insert
vector数据结构如下图所示
3.2 unordered_map
成员函数:
- 迭代器
begin 返回指向容器起始位置的迭代器(iterator)
end 返回指向容器末尾位置的迭代器
cbegin 返回指向容器起始位置的常迭代器(const_iterator)
cend 返回指向容器末尾位置的常迭代器 - Capacity
size 返回有效元素个数
max_size 返回 unordered_map 支持的最大元素个数
empty 判断是否为空 - 元素访问
operator[] 访问元素
at 访问元素 - 元素修改
insert 插入元素
erase 删除元素
swap 交换内容
clear 清空内容
emplace 构造及插入一个元素
emplace_hint 按提示构造及插入一个元素 - 操作
find 通过给定主键查找元素,没找到:返回unordered_map::end
count 返回匹配给定主键的元素的个数
equal_range 返回值匹配给定搜索值的元素组成的范围 - Buckets
bucket_count 返回槽(Bucket)数
max_bucket_count 返回最大槽数
bucket_size 返回槽大小
bucket 返回元素所在槽的序号
load_factor 返回载入因子,即一个元素槽(Bucket)的最大元素数
max_load_factor 返回或设置最大载入因子
rehash 设置槽数
reserve 请求改变容器容量
4.基本算法
4.1 排序算法
4.1.1 插入排序
核心思想是每次选择一个元素,将其插入到合适的位置,遍历完之后也就排好序了。
时间复杂度:O(n^2)
void InsertSort(int data[], int n)
{
int i = 0, j = 0;
for (i = 1; i < n; i++)
{
int temp = data[i];
for (j = i - 1; j >= 0 && data[j] > temp; j--)
{
data[j + 1] = data[j];
}
data[j + 1] = temp;
}
}
int main()
{
int nums[10] = { 1,2,5,3,4,9,8,7,5,6 };
InsertSort(nums, 10);
for (int i = 0; i < 10; i++)
{
cout << nums[i] << endl;
}
return 0;
}
4.1.2 选择排序
选择排序思想:从左到右遍历,每次选择值最小的与最左边的元素交换,这样交换之后最小的会排在第一位
void selectSort(int nums[], int len)
{
for (int i = 0; i < len-1; i++)
{
int minIndex = i+1;
// 选择最小的元素
for (int j = minIndex; j < len; j++)
{
if (nums[j] < nums[minIndex])
minIndex = j;
}
if (i != minIndex)
{
swap(nums[i], nums[minIndex]);
}
}
}
int main()
{
int nums[10] = { 1,5,4,3,6,8,7,1,2,6 };
selectSort(nums, 10);
for (int num : nums)
{
cout << num << " ";
}
return 0;
}
4.1.3 冒泡排序
核心思想是从左向右遍历,每次比较相邻的两个元素,如果 nums[i+1] > nums[i], 则交换,这样交换到最后,一定是最大的放在最后面。
void BlutSort(int nums[], int len)
{
for (int i = 0; i < len-1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
if (nums[j] > nums[j + 1])
{
swap(nums[j], nums[j + 1]);
}
}
}
}
int main()
{
int nums[10] = { 1,5,4,3,6,8,7,1,2,6 };
BlutSort(nums, 10);
for (int num : nums)
{
cout << num << " ";
}
return 0;
}
4.1.4 快速排序
快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
1)数组已经是正序(same order)排过序的。
2)数组已经是倒序排过序的。
3)所有的元素都相同(1、2的特殊情况)
核心思想:选择一个基准元素(通常最左边),每次交换之后,数组中比基准小的都在左边,比其大的都在右边, 然后分别对左边和右边的元素递归排序
#include<iostream>
#include<algorithm>
using namespace std;
int PattenSort2(int r[], int low, int high)//优化过的快速排序
{
int i = low, j = high;
int pivot = r[low];
while (i < j)
{
while (i < j && r[j] > pivot) j--;
while (i < j && r[i] <= pivot) i++; // 注意这里有等于号
if (i < j)
swap(r[i++],r[j--]);
}
if (r[i] > pivot)
{
swap(r[i - 1], r[low]);
return i - 1;
}
swap(r[i], r[low]);
return i;
}
void QuickSort(int r[], int low, int high)
{
int mid;
if (low<high)
{
mid = PattenSort2(r, low, high);
QuickSort(r, low, mid - 1);
QuickSort(r, mid + 1, high);
}
}
int main()
{
int nums[10] = { 1,5,4,3,6,8,7,1,2,6 };
QuickSort(nums, 0, 9);
for (int num : nums)
{
cout << num << " ";
}
return 0;
}
4.1.5 归并排序
归并排序的核心思想是讲数据先拆分,拆分到一个一个的元素为止,再进行合并。
// 将数组合并
void Merge(int nums[], int low, int mid, int high)
{
int i = low, j = mid+1;
int* temp = new int[high - low + 1]; // 构建一个临时数组
int k = 0;
while (i <= mid && j <= high)
{
if (nums[i] < nums[j])
{
temp[k++] = nums[i++];
}
else
{
temp[k++] = nums[j++];
}
}
while (i <= mid)
{
temp[k++] = nums[i++];
}
while (j <= high)
{
temp[k++] = nums[j++];
}
for (int c = low, k=0; c <= high; c++)
{
nums[c] = temp[k++]; // 将临时数组赋值给原数组
}
delete[] temp;
}
// 将数组拆分
void MergeSort(int nums[], int low, int high)
{
if (low < high)
{
int mid = low + (high - low) / 2;
MergeSort(nums, low, mid);
MergeSort(nums, mid + 1, high);
Merge(nums, low, mid, high);
}
}
int main()
{
int nums[10] = { 1,5,4,3,6,8,7,1,2,6 };
MergeSort(nums, 0, 9);
for (int num : nums)
{
cout << num << " ";
}
return 0;
}
4.1.5 堆排序
堆排序的思想就是要首先创建一个大顶堆(如果是从小到大排的话),那怎么根据数组创建大顶堆呢,一般我们规定数组的下标从1开始,然后不断地去执行下沉操作,创建完大顶堆后,再将第一个元素和最后一个元素交换,再执行下沉操作,依次类推。
class Sink(int k, int n) // 下沉
{
while (2*k <= n) // 从最后一个节点的父节点开始下沉
{
int j = 2*k;
if (j < n && nums[j] < nums[j+1]) // 如果有右孩子且右孩子比左孩子大
{
j++;
}
if (r[k] > r[j]) // 如果父节点比最大的孩子都要大,那说明已经是大顶堆了,不需要调整了
{
break;
}
else
{
swap(r[j], r[k]); // 否则就要交换父节点和孩子的位置,这样才能构成大顶堆
}
k = j; // 父节点下沉到孩子节点的位置,继续构建大顶堆
}
}
void CreateHeap(int n)
{
// 构建大顶堆,从最后一个节点的父节点开始调整,下标从1开始的,因此到调整到1为止
for (int j=n/2; j>0; j--)
{
Sink(j, n);
}
}
void HeapSort(int n)
{
// 首先创建堆
CreateHeap(n);
while (n > 1)
{
swap (nums[1], nums[n--]); // 根节点和最后一个孩子节点交换位置,也就是将最大的放到最后了
Sink(1, n); // 调整位置后更新大顶堆
}
}
5. 智能指针
5.1 shared_ptr<>
shared_ptr指针采用的是共享所有权来管理所指向对象的生存期。所以,对象不仅仅能被一个特点的shared_ptr所拥有,而是能够被多个shared_ptr所拥有。
在决定是否采用这种智能指针时首先得想一个问题:所指向的对象是否需要被共享,如果需要被共享(类似于多个指针都需要指向同一块内存) ,那就使用 shared_ptr. 如果只需要独享(只有一个指针指向这块内存) ,就建议使用后面 即 将讲到的 unique_ptr 智能指针,因为 shared_ ptr 虽然额外开销不大,但毕竟为了共享还是会有一些额外开销.
shared_ptr的工作机制是使用引用计数 , 每一个shared_ptr指向相同的对象(内存) ,所
以很显然,只有最后一个指向该对象的 shared_ ptr 指针不需要再指向该对象时,这个shared_ptr 才会去析构所指向的对象.
5.1.2 初始化
shared_ptr p1;这种默认初始化的情形,该智能指针里面保存的是一个空指针nullptr。
1 、常规初始化
智能指针必须进行显示转换,隐式转换不行
例如 shared_ptr p1 = new int(10) 就不行,因为是隐式转换
// p1指向一个值为100的int型指针
shared_ptr<int> p1(new int(100));
// 使用裸指针进行初始化
shared_ptr<int> p1(new int);
2、make_shared函数
这是标准库里的函数模板,被认为是最安全和最高效的分配和使用shared_ptr智能指针的模板。除非指定自己的delete functor,否则我们应该尽量使用std::make_shared。
例如
// p1指向一个值为100的整型的内存,类似int* p1 = new int(100);
shared_ptr<int> p1 = make_shared<int>(10);
// p2指向5个字符a的内存空间。注意到,make_shared后圆括号内的参数取决于<>中的类型名。此时这些参数必须和<>中的类中的某个构造函数匹配
shared_ptr<string> p2 = make_shared<string>(5,'a');
//默认指向类型为int 值为0的空间
shared_ptr<int> p3 = make_shared<int>();
p3 = make_shared<int>(100);//p3释放刚刚的内存,并重新指向新对象
auto sp3 = std::make_shared<int[]>(10);
5.1.3 常用接口
1、 use_count成员函数
该成员函数用于返回多少个指针指针指向某个对象。
2、unique成员函数
是否该指针独占某个指向的对象,也就是若只有一个智能指针指向某个对象,则unique返回true,否则返回false。
3、reset成员函数
(1)当reset不带参数时
若pi是唯一指向该对象的指针,则释放pi指向的对象,将pi置空。
若pi不是唯一指向该对象的指针,则不释放pi指向的对象,但指向该对象的引用计数会减1,同时将pi置空。
(2)当reset带参数(一般是一个new出来的指针)
若pi是唯一指向该对象的指针,则释放pi指向的对象,让pi指向新内存。
若pi不是唯一指向该对象的指针,则不释放pi指向对象,但指向该对象的引用计数会减1,同时让pi指向新内存。
(3)空指针也可以通过reset来初始化
4、get成员函数
p.get(), 返回p中保存的指针。若智能指针释放了所指向的对象,则返回的这个指针所指向的对象也就变得无效了。
要注意:不要delete这个get到的指针
5、swap成员函数
用于交换两个智能指针指向的对象。因为是交换,所以引用计数并不发生变化
6、nullptr
作用:将所指向的对象的引用计数减1,若引用计数变为0,则释放智能指针所指向的对象,并且可以将智能指针置空。
5.1.4 指定删除器
用shared_ptr管理动态数据时,就要用自定义删除器。
5.1.5 shared_ptr使用注意
(1)慎用裸指针
当将一个裸指针绑定到智能指针上时,就尽量不要再用这个裸指针去操作这块内存,更不能用直接删除这个裸指针,否则会造成内存重复释放。同时不能用同一个裸指针去初始化多个智能智能,这会造成指向这块空间的智能指针之间的关系是非关联性的(就会造成内存重复释放),我们所期待的智能指针之间的关系是关联性的。
(2) 慎用get()返回的指针
get()返回的指针不能delete,也不能赋给另外一个智能指针
(3)用enable_shared_from_this 返回this
当在类内需要返回当前对象指针时,如果用this返回,在类外调用这个智能指针时,会使得两个智能指针指向同一块内存,这会导致智能指针指向的空间释放两次。这个时候就要用到enable_shared_from_this。
enable_shared_from_this的原理是,其内部有一个weak_ptr类型的成员变量_Wptr,当shared_ptr构造的时候,如果其模板类型继承了enable_shared_from_this,则对_Wptr进行初始化操作,这样将来调用shared_from_this函数的时候,就能够通过weak_ptr构造出对应的shared_ptr。
5.2 weak_ptr<>
weak_ptr是辅助shared_ptr工作的,也就是说wead_ptr不能单独存在,它必须指向一个shared_ptr管理的对象,并且weak_ptr不管理对象的生存周期。即,将weak_ptr绑定到shared_ptr并不会改变shared_ptr的引用计数。当shared_ptr需要释放所指向的对象时照常释放,不管有没有weak_ptr指向。
weak_ptr的创建一般都是用make_shared创建,如下
此外,weak_ptr所指向的对象有可能不存在,因此不能用weak_ptr来直接访问对象
5.2.1 weak_ptr常用接口(函数)
1 use_count()函数
获取当前所观测资源的强引用(shared_ptr)计数.
2 expired函数
判断所观测的对象是否被释放
3 reset函数
将弱引用计数置空,不影响强引用计数
3 lock()函数
获取所监视的shared_ptr
5.3 unique_ptr
unique_ptr出了上述自己实现的方法外,C++ 提供了一个 move() 库函数,可用于将对象的所有权从一个独占指针转移到另外一个独占指针。
通过move方法可以让unique_ptr被当成参数传递到方法内部,而不用担心所有权被转移而无法继续使用的问题。
// 通过move来转移unique指针的所有权
std::unique_ptr<int> foo(new int(22));
std::unique_ptr<int> bar = std::move(foo); // foo null, bar 22
6. C++ 面试题
6.1 c++多态
C++多态分:静态多态和动态多态
静态多态:函数重载、函数模板,都是在编译器间完成的,函数重载是因为C++编译器在编译时,会对函数名进行扩展,比如fun(int,int)——>fun_int_int,以此来区分不同的函数;函数模板是因为C++编译器会进行两次编译,对于第二次编译会按照不同的函数参数类型,生成多个对应的函数,以此来区分。
动态多态:指程序在运行期间,根据父类指针指向的对象,来判定会执行哪个对象的虚函数。
实现动态多态三个条件:
要有继承
要有虚函数重写
用父类指针(引用)指向子类对象
6.2 重载、重写、隐藏的区别
重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
重写:是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
隐藏(覆盖):是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
重载和重写的区别:
(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。
(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。
(3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。
隐藏和重写,重载的区别:
(1)与重载范围不同:隐藏函数和被隐藏函数在不同类中。
(2)参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。
6.3 引用和指针的区别
C++primer中对 对象的定义:对象是指一块能存储数据并具有某种类型的内存空间
一个对象a,它有值和地址&a,运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值
★相同点:
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存(对象)的别名。
★不同点:
●指针是一个实体,而引用仅是个别名;
●引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
●引用不能为空,指针可以为空;
●“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
●指针和引用的自增(++)运算意义不一样;
●引用是类型安全的,而指针不是 (引用比指针多了类型检查)
指针传递和引用传递:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
6.4 内存对齐
原则一:结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定 会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。
比如此例,
例一:
#include <iostream>
using namespace std;
struct X
{
char a;
int b;
double c;
}S1;
void main()
{
cout << sizeof(S1) << endl;
cout << sizeof(S1.a) << endl;
cout << sizeof(S1.b) << endl;
cout << sizeof(S1.c) << endl;
}
比如例一中的结构体变量S1定义之后,经测试,会发现sizeof(S1)= 16,其值不等于sizeof(S1.a) = 1、sizeof(S1.b) = 4和 sizeof(S1.c) = 8三者之和,这里面就存在存储对齐问题。
首先系统会将字符型变量a存入第0个字节(相对地址,指内存开辟的首地址);然后在存放整形变量b时,会以4个字节为单位进行存储,由于第一个四字节模块已有数据,因此它会存入第二个四字节模块,也就是存入到4~8字节;同理,存放双精度实型变量c时,由于其宽度为8,其存放时会以8个字节为单位存储,也就是会找到第一个空的且是8的整数倍的位置开始存储,此例中,此例中,由于头一个8字节模块已被占用,所以将c存入第二个8字节模块。整体存储示意图如图1所示。
考虑另外一个实例。
例二:
struct X
{
char a;
double b;
int c;
}S2;
在例二中仅仅是将double型的变量和int型的变量互换了位置。测试程序不变,测试结果却截然不同,sizeof(S2)=24,不同于我们按照原则一计算出的8+8+4=20,这就引出了我们的第二原则。
原则二:在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
掌握了这两个原则,就能够分析所有数据存储对齐问题了。再来看几个例子,应用以上两个原则来判断。
例三:
struct X
{
double a;
char b;
int c;
}S3;
首先根据原则一来分析。按照定义的顺序,先存储double型的a,存储在第0-7个字节;其次是char型的b,存储在第8个字节;接下来是int型的c,顺序检查后发现前面三个四字节模块都被占用,因此存储在第4个四字节模块,也就是第12~15字节。按照第一原则分析得到16个字节,16正好是最宽元素a的宽度8的整数倍,因此结构体变量S3所占存储空间就是16个字节。
例四:
struct X
{
double a;
char b;
int c;
char d;
}S4;
仍然首先按照第一原则分析,得到的字节数为8+4+4+1=17;再按照第二原则补齐,则结构体变量S4所占存储空间为24。
例五:
struct X
{
double a;
char b;
int c;
char d;
int e;
}S5;
同样结合原则一和原则二分析,可知在S4的基础上在结构体内部变量定义最后加入一个int型变量后,结构体所占空间并未增加,仍为24。
6.5 c++中内存区域
一、在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
1.栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.堆(C里面的概念),就是那些由malloc分配的内存块,一般一个malloc就要对应一个free.
3.自由存储区(C++引入的概念),就是那些由new等分配的内存块,他和堆是十分相似的
4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)
二、谈谈“C++ 自由存储区是否等价于堆?”
事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是new/delete与malloc/free。然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么请问:借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?
从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。
三、堆和栈的对比
从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而栈是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的! 堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。
1.碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以>参考数据结构,这里我们就不再一一讨论了。
2.生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
3.分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
4.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
6.6 static和const关键字
=static和const的区别
static
1.static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中
2.static 全局变量 表示一个变量在当前文件的全局内可访问
3.static 函数 表示一个函数只能在当前文件中被访问
4.static 类成员变量 表示这个成员为全类所共有
5.static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量
static关键字的作用:
(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问;
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
const
1.const 常量:定义时就初始化,以后不能更改。
2.const 形参:func(const int a){};该形参在函数里不能改变
3.const修饰类成员函数:该函数对成员变量只能进行只读操作
const关键字的作用:
(1)阻止一个变量被改变
(2)声明常量指针(指针指向的值不可以改变,地址可以改变)和指针常量(地址不可以改变,值可以改变)
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。
6.7 不能声明为虚函数的类型
普通函数(非成员函数)
静态成员函数
内联成员函数
构造函数
友元函数。
1)普通函数(非成员函数)只能overload(重载),不能被override(覆盖),不能被声明为虚函数,因此,编译器会在编译时绑定函数。
2)静态成员函数不能是虚函数,因为静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,它不归某个对象所有,所以,它也没有动态绑定的必要性。
3)内联成员函数不能是虚函数,因为内联函数本身就是为了在代码中直接展开,减少函数调用花费的代价而设立的,而虚函数是为了在继承后对象能够准确地执行自己的动作,这是不可能统一的。再说,inline函数在编译时被展开,虚函数在运行时才能动态地绑定函数。
4)构造函数之所以不能是虚函数,虚函数存在于虚表中,而虚表要靠虚表指针维护,而只有实例化后的对象才有虚表指针,而实例化对象就必须调用构造函数初始化对象,所以冲突了,结果就是构造函数不能是虚函数。
5)友元函数。C++语言不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数,不能被继承。所以,友元函数不能是虚函数。
6.8 C++虚函数的原理
虚函数由虚表管理,这张表存放着所有虚函数的地址,而类的实例对象维护着一个虚表指针(也就是说只有实例化的对象才有虚指针),指向这个表。运行的时候,根据new的对象里面的虚表指针,确定调用的函数。这会涉及到一个地址早绑定和地址晚绑定,如下所示,父类animal和子类都有一个函数bark(),如果该函数不是虚函数,那么在编译阶段(可以理解为代码写完还没有运行的阶段),地址就已经确定了,而由于animal和Dog类还没有实例化(类的实例化在运行阶段),但由于我们定义了animal a,因此编译器会将a划分为animal的指针,也就是运行时会运行animal的bark()函数,而加了virtual之后,编译器在编译阶段无法找到bark()的地址,只能在运行时根据a指定的类来确定运行哪个bark
animal a = new Dog;
a->bark();
如下例子所示:
class A
{
public:
virtual void f1() { cout << "A...f1()" << endl; }
};
class B :public A
{
public:
void f1() { cout << "B...f1()" << endl; }
};
int main()
{
A* a = new A; //因为不是抽象类,所以可以实例化
B* b = new B; //直接用子类自己实例化
A* pB = new B; //用基类指针指向子类
return 0;
}
这就是为什么能在运行时确定调用哪个函数的原因了,当new B返回B的对象后,里面会有一张B类的虚表,然后根据B的虚表调用B的函数。
更多可见:https://www.zhihu.com/answer/84332610
C++ 虚函数表剖析 - 知乎
https://zhuanlan.zhihu.com/p/75172640
6.9栈溢出
栈溢出泛指系统维护的栈溢出,因数据压不下去了,导致溢出,此时程序会崩溃。
一般递归深度过大、创建普通数组过大(就是局部变量占用的空间大于栈了就会溢出,new的是堆,不算)。
6.10 创建一个不可被继承的类
(1)第一种方法,将类的构造函数和析构函数私有化
如下所示,只要将父类的构造函数和析构函数私有化,那么父类就无法被继承。
class Base
{
private:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Son1 : public Base
{
public:
Son1() { cout << "Son构造" << endl; }
~Son1() { cout << "Son析构" << endl; }
};
这种情况虽然可以使得基类无法被继承,但是问题是,基类无法被继承的同时,基类页无法实例化对象了(因为构造析构私有化了),此时如果想要构造函数和析构函数私有化的基类实例化,可以采用下面办法,即类似于单例模式
class OnlyHeapClass {
public:
static OnlyHeapClass* GetInstance() {
//创建一个OnlyHeapClass对象并返回其指针
return (new OnlyHeapClass);
}
void Destroy();
private:
OnlyHeapClass() { cout << " OnlyHeapClass构造" << endl; };
~OnlyHeapClass() { cout << " OnlyHeapClass析构" << endl; };
};
// 手动析构
void OnlyHeapClass::Destroy() {
delete this;
}
int main()
{
OnlyHeapClass * ptr = OnlyHeapClass::GetInstance(); // 实例化
ptr->Destroy(); // 手动析构
return 0;
}
(2)利用友元不可以被继承
第一种方法虽然基类无法被继承,但同时实例化也不方便,下面本文要说明不可被继承类的第二种方式。利用友元的特性:友元不能被继承。什么是友元不能被继承性呢?利用了虚继承的一个特征就是虚基类的构造函数由最终子类负责构造。
class CFinalClassMixin {//从这个类中继承的类都不能再被继承
friend class Cparent; // 必须要声明友元类,否则Cparent就先无法继承CFinalClassMixin了,因为CFinalClassMixin的构造析构是私有化的
private: // 要私有化其其构造析构
CFinalClassMixin() {}
~CFinalClassMixin(){}
};
class Cparent: virtual public CFinalClassMixin {
public:
Cparent() {}
~Cparent(){}
};
class CChild : public Cparent {
public:
CChild() {};//编译错误
~CChild() {};//编译错误
};
如上所示,利用了友元类不能被继承,注意为什么用虚继承?如果不是虚继承,那么CChild直接调用Cparent的构造函数,这是成立的,而且CChild是不需要调用CFinalClassMixin的构造函数。若把它声明为虚继承,那么CChild就必须负责CFinalClassMixin构造函数的调用(虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。),这样又因为不能继承friend类,所以不能调用,造成了错误。
此外, 必须要声明友元类Cparent,否则Cparent就先已经无法继承CFinalClassMixin了,因为CFinalClassMixin的构造析构是私有化的,这样就无法得出Cparent无法被继承了。
7. 设计模式
7.1 工厂模式
工厂模式的主要作用是封装对象的创建,分离对象的创建和操作过程,用于批量管理对象的创建过程,便于程序的维护和扩展。
工厂模式主要解决接口选择的问题。该模式下定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,使其创建过程延迟到子类进行。
简单来说,就是将多个共同类型的类(比如苹果,香蕉,菠萝等都属于水果),将它们封装成一个虚基类,然后这些具体类再去继承这个虚基类,虚基类对外提供水果的接口。接下来再封装一个工厂类,在工厂中根据不同的参数去生产不同的类
7.1.1 简单工厂模式
特点:
- 客户端和具体实现类解耦
- 对于某些创建类的复杂情况,我们不需要考虑,只需传入参数即可
简单工厂模式的缺点:
- 简单工厂模式,增加新的功能是通过修改工厂类来实现的,及修改了工厂类的代码,这不符合开闭原则。
- 只有一个工厂,工厂负担过重,它承担所有的类实现,万一出现问题,则所有的类都无法使用了
#include<iostream>
#include<string>
using namespace std;
/*简单工厂模式*/
// 抽象水果,定义为抽象类,仅仅起到了一个接口的作用(继承类的接口),没有其他任何作用,抽象类一般为基类
class AbstractFruit {
public:
virtual void ShowName() = 0;
};
// 苹果
class Apple :public AbstractFruit {
public:
void ShowName()
{
cout << "我是苹果" << endl;
}
};
// 香蕉
class Banana :public AbstractFruit {
public:
void ShowName()
{
cout << "我是香蕉" << endl;
}
};
// 工厂
class FruitFactory {
public:
static AbstractFruit* CreateFruit(string flag)
{
if (flag == "apple") {
return new Apple;
}
else if (flag == "Banana")
{
return new Banana;
}
else {
return NULL;
}
}
};
void test01() {
FruitFactory* factory = new FruitFactory;
// 生产水果
AbstractFruit* fruit = factory->CreateFruit("apple");
fruit->ShowName();
// 生产香蕉
fruit = factory->CreateFruit("Banana");
fruit->ShowName();
}
int main()
{
test01();
return 0;
}
7.1.2 工厂方法模式
工厂方法模式讲工厂也定义成抽象类
特点:
- 符合开闭原则(工厂是一个抽象类,不需要修改工厂代码)
- 便于后期产品种类的扩展。
缺点:
- 类的个数成倍增加,导致类越来越多,维护成本高(因为一个实例工厂只生产一个产品,每增加一个产品就要增加一个实例工厂,导致类太多了)
#include<iostream>
#include<string>
using namespace std;
/*简单工厂模式*/
// 抽象水果,定义为抽象类,仅仅起到了一个接口的作用(继承类的接口),没有其他任何作用,抽象类一般为基类
class AbstractFruit {
public:
virtual void ShowName() = 0;
};
// 苹果
class Apple :public AbstractFruit {
public:
void ShowName()
{
cout << "我是苹果" << endl;
}
};
// 香蕉
class Banana :public AbstractFruit {
public:
void ShowName()
{
cout << "我是香蕉" << endl;
}
};
// 讲工厂也定义为抽象类
class AbstractFactory {
public:
virtual AbstractFruit* CreateFruit() = 0;
};
// 生产苹果的工厂
class appleFactory :public AbstractFactory {
public:
AbstractFruit* CreateFruit() {
return new Apple;
}
};
// 生产香蕉的工厂
class balanaeFactory :public AbstractFactory {
public:
AbstractFruit* CreateFruit() {
return new Banana;
}
};
void test01() {
AbstractFactory* factory = NULL;
AbstractFruit* fruit = NULL;
// 生产苹果
factory = new appleFactory;
fruit = factory->CreateFruit();
fruit->ShowName();
// 生产香蕉
factory = new balanaeFactory;
fruit = factory->CreateFruit();
fruit->ShowName();
}
int main()
{
test01();
return 0;
}
7.1.3 抽象工厂方法模式
抽象工厂方法就是将一族抽象成一个基类(比如说苹果,有美国苹果,中国苹果,把所有的苹果抽象成一族);然后再把工厂簇抽象成一个基类,工厂中包含了产品簇的方法
#include<iostream>
#include<string>
using namespace std;
/*抽象工厂方法*/
// 抽象苹果类
class AbstractApple
{
public:
virtual void showName() = 0;
};
class ChinaApple : public AbstractApple{
public:
void showName()
{
cout << "中国苹果" << endl;
}
};
class USAApple :public AbstractApple {
public:
void showName()
{
cout << "USA苹果" << endl;
}
};
// 抽象香蕉类
class AbstractBanana
{
public:
virtual void showName() = 0;
};
class ChinaBanana :public AbstractBanana {
public:
void showName()
{
cout << "中国香蕉" << endl;
}
};
class USABanana :public AbstractBanana {
public:
void showName()
{
cout << "USA香蕉" << endl;
}
};
// 抽象工厂,针对产品族
class AbstrctFactory {
public:
virtual AbstractApple* CreateApple() = 0;
virtual AbstractBanana* CreateBanana() = 0;
};
// 实例化中国工厂(只生产中国产品)
class ChinaFactory : public AbstrctFactory {
public:
virtual AbstractApple* CreateApple()
{
return new ChinaApple;
}
virtual AbstractBanana* CreateBanana()
{
return new ChinaBanana;
}
};
// 实例化美国工厂(只生产美国产品)
class USAFactory : public AbstrctFactory {
public:
virtual AbstractApple* CreateApple()
{
return new USAApple;
}
virtual AbstractBanana* CreateBanana()
{
return new USABanana;
}
};
void test()
{
// 中国工厂
AbstrctFactory* factory = new ChinaFactory;
// 生产中国苹果
AbstractApple* apple = factory->CreateApple();
apple->showName();
// USA工厂
factory = new USAFactory;
// 生产美国苹果
apple = factory->CreateApple();
apple->showName();
}
int main()
{
test();
return 0;
}
7.2 观察者模式
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时(观察目标),所有依赖于它的对象都得到通知并被自动更新(观察者)。
#include<iostream>
#include<string>
#include<list>
using namespace std;
// 抽象观察者
class AbstractHero {
public:
virtual void Update() = 0;
};
// 实例化观察者A
class HeroA : public AbstractHero {
public:
virtual void Update()
{
cout << "英雄A更新" << endl;
}
};
// 实例化观察者B
class HeroB : public AbstractHero {
public:
virtual void Update()
{
cout << "英雄B更新" << endl;
}
};
// 实例化观察者C
class HeroC : public AbstractHero {
public:
virtual void Update()
{
cout << "英雄C更新" << endl;
}
};
// 抽象观察目标(观察目标可以对观察者的状态进行改变,还可以对观察者进行管理)
class AbstractBoss {
public:
/*定义管理方法*/
// 删除观察者
virtual void DeleteHero(AbstractHero* hero) = 0;
// 添加观察者
virtual void AddHero(AbstractHero* hero) = 0;
// 通知所有观察者
virtual void NotifyAll() = 0;
};
// 具体的被观察目标
class Boss : public AbstractBoss{
public:
/*定义管理方法*/
// 删除观察者
virtual void DeleteHero(AbstractHero* hero)
{
pHeraoList.remove(hero);
}
// 添加观察者
virtual void AddHero(AbstractHero* hero)
{
pHeraoList.push_back(hero);
}
// 通知所有观察者
virtual void NotifyAll()
{
// 循环遍历链表中所有元素
for (auto it = pHeraoList.begin(); it != pHeraoList.end(); it++)
{
(*it)->Update();
}
}
public:
list<AbstractHero*> pHeraoList;
};
void test01()
{
// 实例化观察者A
AbstractHero* A = new HeroA;
// 实例化观察者B
AbstractHero* B = new HeroB;
// 实例化观察目标
AbstractBoss* boss = new Boss;
// 观察目标对观察者进行相应的管理
boss->AddHero(A);
boss->AddHero(B);
boss->NotifyAll();
boss->DeleteHero(B);
boss->NotifyAll();
}
int main()
{
test01();
return 0;
}
7.3 代理模式
#include<iostream>
#include<string>
using namespace std;
/*代理模式*/
// 通用接口,通过这个通用接口去调用系统。但是用户无法通过这个接口去直接访问,只能通过代理来访问这个接口
class AbstractCommonInterface {
public:
virtual void run() = 0;
};
// 具体系统
class MySystem :public AbstractCommonInterface {
public:
void run()
{
cout << "系统运行" << endl;
}
};
// 用户只能通过代理接口去访问通用接口,并且需要对用户进行验证,只有合法用户才能访问系统
class SystemProxy :public AbstractCommonInterface {
public:
SystemProxy(string name, string passwd)
{
this->userName = name;
this->userPasswd = passwd;
pSystem = new MySystem;
}
~SystemProxy()
{
if (pSystem != NULL)
{
delete pSystem;
}
}
bool IsValid()
{
if (userName == "user" && userPasswd == "pass")
{
return true;
}
else
{
return false;
}
}
void run()
{
if (IsValid())
{
pSystem->run();
}
else {
cout << "非合法用户" << endl;
}
}
public:
AbstractCommonInterface* pSystem;
string userName;
string userPasswd;
};
void test()
{
AbstractCommonInterface* face = new SystemProxy("user", "pass");
face->run();
}
int main()
{
test();
return 0;
}
7.4 单例设计模式
7.4.1 经典的懒汉单例设计模式
#include<iostream>
#include<string>
using namespace std;
class SingelMod
{
private:
SingelMod() {};
static SingelMod* m_singel;
public:
// 对外提供获取对象的接口
static SingelMod* GetInstance()
{
if (m_singel == nullptr)
{
m_singel = new SingelMod;
}
return m_singel;
}
};
SingelMod* SingelMod::m_singel = nullptr;
int main()
{
SingelMod* singel1 = SingelMod::GetInstance();
SingelMod* singel2 = SingelMod::GetInstance();
if (singel1 == singel2)
{
cout << "相同" << endl;
}
return 0;
}
7.4.2 线程安全的懒汉单例设计模式
经典的懒汉设计模式在单线程下不存在安全隐患,但是在多线程下就会存在隐患,是不安全的
1 #include<pthread.h>
2 #include<iostream>
3 using namespace std;
4
5 class SingelThread
6 {
7 private:
// 静态变量,要在类外初始化
8 static pthread_mutex_t mutex;
9 SingelThread(){
10 pthread_mutex_init(&mutex, NULL);
11 }
12 static SingelThread* p;
13 public:
14 static SingelThread* GetInstance()
15 {
// 双重加锁
16 if (p == NULL)
17 {
18 pthread_mutex_lock(&mutex);
19 if (p == NULL)
20 {
21 p = new SingelThread;
22 }
23 pthread_mutex_unlock(&mutex);
24 }
25 return p;
26 }
27 };
28 pthread_mutex_t SingelThread::mutex;
29 SingelThread* SingelThread::p = NULL;
30
31 void* func1(void* arg)
32 {
33 SingelThread* singel1 = SingelThread::GetInstance();
34 cout << "singel1:" << singel1 << endl;
35 return (void*)0;
36 }
37 void* func2(void* arg)
38 {
39 SingelThread* singel2 = SingelThread::GetInstance();
40 cout << "singel1:" << singel2 << endl;
41 return (void*)0;
42 }
43
44 int main()
45 {
46 pthread_t pth1, pth2;
47 pthread_create(&pth1, NULL, func1, NULL);
48 pthread_create(&pth2, NULL, func2, NULL);
49 pthread_join(pth1, NULL);
50 pthread_join(pth2, NULL);
51
52 cout << "主线程" << endl;
53 return 0;
54 }
7.4.2 线程安全的饿汉单例设计模式
#include<iostream>
#include<string>
using namespace std;
class SingelMod
{
private:
SingelMod() {}
~SingelMod() {}
static SingelMod* p;
public:
static SingelMod* GetInstance()
{
return p;
}
};
SingelMod* SingelMod::p = new SingelMod;
int main()
{
SingelMod* p1 = SingelMod::GetInstance();
SingelMod* p2 = SingelMod::GetInstance();
if (p1 == p2)
{
cout << "相同" << endl;
}
return 0;
}