这里写目录标题
面向对象的三大特性
- 封装
降低耦合性;通过将实现和数据封装成函数,只能通过接口进行访问。 - 继承
提高耦合性;子类继承父类的特征和行为。在C++的类中,设置了访问修饰符,分别是Public(公共的、类外可以访问)、private(私有的、类外不能访问但是只有本类可以访问 )、proteted(受保护的、类外不能访问但是派生类可以访问),子类可以通过继承父类访问非private的方法或成员变量,重写父类方法。
注意:当父类中的成员变量、成员函数或其本身被 final 关键字修饰时,类不能被继承,其成员也不能被重写或修改。 - 多态
即是多样化的继承,对于同一消息做出的不同回应。多态通过虚函数实现,虚函数是在基类中声明为虚函数的成员函数,它可以在派生类中被重写。通过基类的指针或引用来调用派生类的方法,实现了代码的灵活性和可扩展性。
类
在 c++ 中,使用类来描述对象的属性,为类添加自定义行为,使用关键词 class 来创建类;
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
int b;
A(int a) //构造函数
{
this->a = a;
cout << "A" << endl;
}
~A() //析构函数
{
cout << "~A" << endl;
}
int show(int b) //成员函数
{
cout << b << " ";
return a * a;
}
private:
int a;
};
int main()
{
A a(5);
a.b = 5;
cout << a.b <<"" <<a.show(a.b) << endl;
return 0;
}
类的定义由关键字(class)、类名 (A)、访问修饰符(public 、private、protected<此关键字比较特殊,我们后续再详细介绍>、变量、函数方法共同构成,公共成员可以直接通过(.) 运算符进行访问。
struct和class的区别
- struct 在c 语言中只支持成员变量的声明,可以使用函数指针,通过结构体变量调用函数指针的方式实现类似成员函数的行为。在c++中,struct 支持成员函数和变量的定义,所有的成员默认是公有的,在继承时也是公有继;而在class 中,成员默认私有。
类的对象
除了上述类的访问修饰符、成员函数外,类的对象还包括下列成员:
构造函数和析构函数
A() //构造函数
{
cout << "A" << endl;
}
~A() //析构函数
{
cout << "~A" << endl;
}
A()为构造函数,每次在创建类的新对象时执行,可以为成员变量提供初始化。
~A()为析构函数,每次在删除类的对象时执行,不返回值,也不能带任何参数。
默认构造函数
从上面的程序我们可以看到,类A中有一个构造函数,我们在构造函数中初始化了变量 a。而默认构造函数就是没有显式提供初始化的构造函数,他们一般没有参数,或者是为所有形参提供默认参数的构造函数定义。
- 未提供参数的默认构造函数
A()
{
cout << "A" << endl;
}
- 为所有形参提供默认参数的构造函数
A(int a =10)
{
this ->a =a;
cout << "A" << endl;
}
以上,是由用户构造的默认构造函数,当用户没有为类提供构造函数时,编译器就会为类自动生成该类的默认构造函数。
class A
{
public:
int a;
};
int main()
{
A aa;
cout<<aa.a<<endl;
}
此时,类中用户没有为类 A 设置任何构造函数,编译器会自动为 A 生成了初始化变量 a 的构造函数,默认赋值为0。
禁止构造函数
我们可以通过添加 “=delete ” 修饰符的方式禁止已经声明了的构造函数。
A()
{
cout << "A" << endl;
}
A(int a,int b)= delete;
那么,我们为什么要禁止构造函数呢?
当用户实现过自定义构造参数后,编译器就不会再生成默认构造函数。当类中含有不能默认拷贝成员变量时,可以禁止默认构造函数的生成。
注意:如果我们仅仅只是将构造函数设置成私有的,仍然被类的内部成员、友元函数访问,无法真正的实现禁止。c++11后,可以通过在成员函数后添加“ =delete ”禁止该函数使用,若需要保留,则添加“ = default ”。
更多关于delete和default
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,通过使用另一个同类型的对象来初始化新创建的对象,复制对象把它作为参数传递给函数,并从函数返回这个对象。
classname (const classname &obj) {
// 构造函数的主体
}
- obj 为一个对象的引用,使用该对象以初始化另一对象。
- 使用 const 关键字是为了确保不改变拷贝的对象,使用const 关键字后可以防止失误操作改变拷贝对象。
- 引用& 可以防止拷贝构造函数无限制递归而引起的栈溢出情况。
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int a) //构造函数
{
this->a = a;
cout << "A" << endl;
}
~A() //析构函数
{
cout << "~A" << endl;
}
int show()
{
return a * a;
}
private:
int a;
};
int main()
{
A a(5);
A c = a;
cout << a.show() << " " << c.show() << endl;
return 0;
}
从上述例子我们可以看到,我们将a 的值赋值给了c 后再类的创建过程中,程序的结果显示类 A 只被构造了1次,但是析构函数确是被执行了两次。这是由于此时编译器调用的是编译器自行构造的拷贝构造函数。
我们可以对上述程序进行修改:
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int b) //构造函数
{
a = b;
cout << "A" << endl;
}
A(const A& b)
{
cout << "拷贝A" << endl;
a = b.a;
}
~A() //析构函数
{
cout << "~A" << endl;
}
int show()
{
return a * a;
}
private:
int a;
};
int main()
{
A a(5);
A c = a;
cout << a.show() << " " << c.show() << endl;
return 0;
}
深拷贝和浅拷贝
当我们进行类对象的拷贝复制时,若重新进行资源的 分配就是深拷贝,反之就是浅拷贝。
浅拷贝
假设原对象为a ,拷贝对象为 b, 在浅拷贝的情况下a 与b 占用同一的内存空间,b 仅拷贝了a 在栈空间上的内容。在a 对象 被删除时,a 所指向的空间会被释放,此时b 指向的就是垃圾内存。
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int b) //构造函数
{
a = (int *)malloc(sizeof(b));
cout << "A" << endl;
}
~A() //析构函数
{
if (a != nullptr)
{
free(a);
}
cout << "~A" << endl;
}
private:
int *a;
};
int main()
{
A a(5);
A b = a;
}
在上面的程序中,b 指向的空间并没有被正常释放,这时因为 a 指向的空间已经被释放了,而b 再去释放已经被释放的空间就会导致程序的崩溃。
而此时我们就可以使用深拷贝的方法解决这一问题。
深拷贝
假设原对象为a ,拷贝对象为 b, 在深拷贝的情况下a 与b 占用不同的内存空间,b既拷贝了 a 在栈中的内容,也拷贝了 a 在堆空间的内容。
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int b) //构造函数
{
this->a = (int*)malloc(sizeof(b));
cout << "A" << endl;
}
A(const A& b)
{
a = (int*)malloc(sizeof(b.a));
cout << "拷贝A" << endl;
}
~A() //析构函数
{
if (a != nullptr)
{
free(a);
}
cout << "~A" << endl;
}
private:
int* a;
};
int main()
{
A a(5);
A b = a;
}
避免拷贝构造
由于编译器自动构造的默认拷贝函数为浅拷贝,所以在特别的情景下,我们需要对默认拷贝构造函数禁止。
避免拷贝构造有以下方法:
- 声明为private 下的私有拷贝构造函数,此时类中的成员函数、友元函数仍可以对其进行调用,此时用户对其进行值传递时会导致程序错误。我们可以在声明为私有的拷贝构造函数后继续声明一个子类私有的继承父类,此时再通过子类进行操作,子类中的成员函数、友元函数并不能对拷贝构造函数进行操作。
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
A();
~A();
private:
A(const A&);
};
class B :private A
{
};
- 使用修饰符“ =delete ”
class A
{
public:
A();
A(const A&)=delete;
~A();
};
友元函数
友元函数使用 friend 关键词修饰,并不是类的成员函数,友元函数既可以是声明为函数的友元函数,也可以是声明为类的友元类。友元函数定义再类外,可以访问类的私有成员(private)和 受保护成员(protected)。
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
{
p = a;
cout << "A" << endl;
}
~A();
void show()
{
cout << "kaka" << endl;
}
friend void fa(A a)
{
cout <<a.p<< endl;
}
friend class FA;
private:
void AA()
{
cout << "aa" << endl;
}
int p;
};
class FA
{
public:
void fshow(A &a)
{
a.AA();
}
};
int main()
{
A s(10);
s.show();
fa(s);
FA fa;
fa.fshow(s);
return 0;
}
内联函数
内联函数使用关键字 “inline” 进行修饰,在类中定义的函数虽然没有 inline 修饰符,但仍然都是内联函数。内联函数使用空间换时间的方法提高了函数的调用效率,函数被声明内联函数后,编译器会将其内联展开替换,而一般的程序是在运行时才被替代。也因此,内联函数多了一些限制:
- 在内联函数内不允许使用循环语句和开关语句;
- 内联函数的定义必须出现在内联函数第一次调用之前;
- 类结构中所在的类说明内部定义的函数是内联函数。
所以,在使用内联函数时我们一般将代码控制在 10 行以内。
#include<bits/stdc++.h>
using namespace std;
inline void temp(int x, int y)
{
int a;
a = x;
x = y;
y = a;
cout << x << " " << y << endl;
}
int main()
{
temp(5, 10);
temp(6, 7);
return 0;
}
this指针
this指针是类的成员函数的隐藏参数,类的每个对象均能通过this指针访问自己的地址,因此我们可以使用它来调用类的对象。
在上面的示例代码中我们就曾使用过this指针。
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int a) //构造函数
{
this->a = a; //参数名与类中私有成员变量名相同,为避免赋值错误,通过使用this指针辨别两者
cout << "A" << endl;
}
~A() //析构函数
{
cout << "~A" << endl;
}
int show()
{
return a * a;
}
int Ashow()
{
return this->show()+ this->a;
}
private:
int a;
};
int main()
{
A a(10);
int b = a.show();
int c = a.Ashow();
cout << b << " " << c << endl;
}
指向类的指针
指向类的指针可以访问类的成员,和普通的指针使用方法一致。
#include<bits/stdc++.h>
using namespace std;
class A
{
public://公共的
A(int a) //构造函数
{
this->a = a;
cout << "A" << endl;
}
~A() //析构函数
{
cout << "~A" << endl;
}
int show()
{
return a * a;
}
int Ashow()
{
return this->show() + this->a;
}
private:
int a;
};
int main()
{
A b(2);
A* p;
p = &b;
cout << b.show() << " " << b.Ashow() << endl;
cout << p->show() << " " << p->Ashow() << endl;
}
类的静态成员
使用 static 关键字修饰,默认静态成员初始化为0。静态成员函数在内存中只有一个副本,属于类本身,即使在类对象不存在的情况下也能被调用,静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
class A{
public:
static int count; // 声明静态数据成员
};
int A::count = 0; // 定义和初始化静态数据成员
int main() {
A::count = 5;
cout << A::count << endl;
return 0;
}
继承
继承即一个派生类(子类)继承基类(父类)属性和行为的操作,通过继承,派生类可以继承基类的成员,并且还可以添加自己的成员。通过继承,实现了重用代码功能和提高执行效率。
继承方式:
- 公有继承(public ):派生类可以访问基类的公有成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。使用关键字public进行声明。
- 私有继承(private):派生类可以访问基类的私有成员,但不能访问基类的公有成员。使用关键字private进行声明。
- 受保护继承(protected):派生类可以访问基类的受保护成员,但不能访问基类的公有成员。使用关键字protected进行声明
多继承
即派生类可以继承自多个基类,从而获得这些基类的成员。
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
#include<bits/stdc++.h>
using namespace std;
// 基类A
class A {
public:
void showA() {
cout << "A" << endl;
}
};
// 基类B
class B {
public:
void showB() {
cout << "B" << endl;
}
};
// 派生类D,继承自基类A和基类B
class D : public A, public B {
public:
void showD() {
cout << "D" << endl;
}
};
int main() {
D d;
d.showA();
d.showB();
d.showD();
return 0;
}
重载函数
C++ 中允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
当我们调用一个重载函数或重载运算符时,编译器会比较所使用的参数类型与定义中的参数类型,决定选用最合适的定义。选择最合适的重载函数或重载运算符,这个过程称为重载决策。
函数重载
对同一函数名声明不同参数的行为。
class A
{
public:
void show()
{
cout << "show" << endl;
}
void show(int a)
{
cout << a << endl;
}
void show(double a )
{
cout << a << endl;
}
};
int main()
{
A a;
a.show();
a.show(4);
a.show(1.5);
}
函数重写(覆盖)
在继承中需要在派生类中重新定义函数是,我们就需要对函数进行重写操作。函数重写函数名、参数都与原函数相同,但是函数体不同。重写时,基类中被重写的函数必须添加virtual 关键字修饰。
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
void show(double a )
{
cout << a << endl;
}
};
class B :public A
{
public:
virtual void show(double a)
{
cout << a * a << endl;
}
};
int main()
{
A a;
B b;
a.show(1.5);
b.show(1.5);
}
函数重载与重写的区别
- 重载发生在同一个类内部,重写发生在基类和父类之间。
- 重载函数与原函数名相同,参数列表不同,不关注函数的返回类型。重写函数与原函数名、参数列表、返回类型均相同,函数体不同,并且需要关键字virtual 修饰。
多态
多态是在继承过程中派生类和基类对同一类型的不同响应。
C++中实现多态的一种方式是通过虚函数(virtual function)。虚函数是在基类中声明的函数,可以被派生类重写(覆盖)。当基类的指针或引用调用虚函数时,实际调用的是派生类中重写的函数。
虚函数
即是使用关键字virtual 声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们知道,在空类的情况下,类的大小为1(占位符),当我们定义了一个虚函数后,类的大小改变(编译器不同结果会有差别)。
#include<iostream>
using namespace std;
class A
{
public:
virtual void show()
{
cout << " virtual show()" << endl;
}
};
int main()
{
cout<<sizeof(A);
}
这是因为虚函数中有一个指向虚函数表的对象——虚函数指针。
虚函数通过虚函数表实现,虚函数表中存放了虚函数的地址。是类实现多态的一个重要机制,虚函数通过派生类继承基类的方式,在派生中重写虚函数,基类通过虚函数指针调用派生类中的虚函数(虚函数表指明了实际调用的函数)
#include<iostream>
using namespace std;
class A
{
public:
virtual void show()
{
cout << "A virtual show()" << endl;
}
};
class B :public A
{
public:
virtual void show()
{
cout << "B virtual show()" << endl;
}
};
int main()
{
A a;
B b;
A aa;
A* pa = &a;
pa->show();
B* pb = &b;
pb->show();
A* paa = &b;
paa->show();
cout << pa << " " << pb<<" "<<paa << endl;
}
构造函数/析构函数 是否可以是虚函数
- 构造函数不可以是虚函数
虚函数的生成时机是在类对象生成之后,而类的构造函数就是为了构造对象,若将析构函数声明为虚的,则在虚析构函数实现时,需要寻找类的对象,这是一个死循环。 - 析构函数可以是虚函数
析构函数执行时机是删除类的对象及其指向的内存块,在继承关系存在时,虚析构函数使删除指向子类对象的基类指针时可以调用子类的析构函数,以释放子类中堆内存的目的,从而也防止了内存泄露的。
纯虚函数
纯虚函数声明在虚函数的基础上让表达式“ =0”,即可实现。
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
~A()
{
cout << "~A" << endl;
}
virtual void show() = 0;
};
class B :A
{
public :
~B()
{
cout << "~B" << endl;
}
virtual void show()
{
cout << "show()" << endl;
}
};
int main()
{
B b;
B* pb = &b;
pb->show();
}
当析构函数被定义为纯虚函数时,该类被定义为抽象类,不能被实例化。派生类必须重写基类中的纯虚函数(全部),否则派生类不能实例化对象。
虚函数与纯虚函数的区别
- 声明上纯虚函数需要在表达式后添加” = 0 “;
- 虚函数可以直接使用;纯虚函数需要在派生类中实现才能使用;
- 虚函数必须实现;纯虚函数在基类中可以不实现(此时该类作为抽象类)。
抽象类(规范化代码)
如果类中至少有一个函数被声明为纯虚函数,该类就是抽象类。
抽象类不能被实例化对象,只作为接口使用。
#include<iostream>
using namespace std;
class A
{
protected:
int a;
int b;
public:
int AA(int a)
{
this->a = a;
return a;
}
int BA(int b)
{
this->b = b;
return b;
}
virtual void add(int num1, int num2) = 0;
};
class B :A
{
public:
virtual void add(int num1,int num2)
{
cout << num1 + num2 << endl;
}
};
int main()
{
B b;
B* pb = &b;
pb->add(1, 2);
}
以上是本人在复习C++内容期间的一些学习笔记,如果内容有错误或需要改进的地方欢迎指正,谢谢大家!