class类
\(oop\),面向对象程序设计
类\(class\)
封装 继承 多态
封装
类将对象的属性和行为封装起来
类和对象的关系相当于数据类型和变量
类的成员:数据成员和函数成员
class /*类名*/{
private:
//行为或属性
protected:
//...
public:
//...
};
访问控制:不标明控制类型默认private
private只能在类内部访问

protected类内部和派生类(继承)可访问
public任意地方可访问
成员访问使用“.”和“->” 变量名.成员名 / 对象指针名->成员名
定义对象数组两种方式:
Box box[3]
Box* p = new Box[3]
构造函数
包括默认构造,有参构造,复制构造
无返回类型,创建对象时自动执行,与类同名
默认构造(无参):
当类中没有自定义构造函数时系统自动生成默认构造函数
一但类中有了构造函数(无论有参无参)系统都不再自动生成
class Time{
//...
public:
Time(){/*可以写语句,赋值等等*/}
//...
};
有参构造:
class Time{
private:
int h,m,s;//时分秒
public:
Time(int h,int m,int s) : h(h),m(m),s(s){
//...
}
自动初始化列表,Time(int h,int m,int s) : h(h),m(m),s(s),括号前是数据成员,括号中是形参,可以规避二者名称相同时h=h的问题(编译器会分不清哪个是成员哪个是形参)

带默认参数的构造函数:
Time(int h=0,int m=0,int s=0):h(h),m(m),s(s){}
Time t1; //调用无参构造,初始化h=0,m=0,s=0
Time t2(1,2,3) //调用有参构造,初始化h=1,m=2,s=3
复制构造函数
也是构造函数,可以创建对象
拷贝构造是必须的,无定义则自动生成默认拷贝构造
class MyClass {
public:
// 默认复制构造函数(编译器自动生成)
MyClass(const MyClass& other)
: member1(other.member1), member2(other.member2) /* 逐个成员初始化 */ {}
};
Time(int h=0,int m=0,int s=0):h(h),m(m),s(s){}
Time(const Timer&t){.....} //拷贝构造
Time(const Timer&t){}必须要传引用

A y = x; 是构造
A y; y = x; 是赋值
深复制与浅复制
浅复制的问题(如图):



obj1和obj2会释放同一块内存,导致系统崩溃
深复制:

应该重新分配空间,将数值复制到新空间内


关于函数参数:


析构函数
无返回类型,无参数,不可重载
默认析构函数~Time(){}
一般不需要写析构函数,只有类内使用了new对指针分配了空间时必须写析构函数进行delete
ex:
对于数据量较大的工程而言,使用new将数据存放再堆中,不使用new默认放入栈中,会溢栈
有new必须有delete

关于友元\(friend\)
友元函数,友元类
友元是给予而非索要,具有非对称性(A是B的友元,B不一定是A的友元),非传递性
友元提供了一种更快捷的访问类内成员的途径,但是破坏的类的封装性,不常使用友元
友元函数:

友元类:

\(this,static,const\)
\(this\)
当一个成员函数被调用时,自动传递一个隐含的参数,该参数是一个指向该对象的指针
只能在类的成员函数中使用,指向对象本身
大量用于运算符重载中
调用:this->num (*this).num


静态成员\(static\)
只说类的静态成员变量
静态成员被所有类共享
class myclass{
private:
int id;
static int count;
public:
myclass(){//构造函数
id = ++count;
//id是每个对象都有一个,count是全局计数器
}
};
int myclass::count = 0;
class myclass{
static int cnt; //类内声明
};
int myclass::cnt = 0;//必须在类外初始化,必须是这种格式
私有静态成员:只能在类内访问,共享与访问控制不同
公有静态成员:可以在类外访问,如在main()函数中调用myclass::cnt是ok的,破环了封装性,不推荐
全局变量:与公有静态成员的作用域不同,在main()函数中直接调用cnt
类的关系

类的组合
本类中包含其它类作为成员
class Point{
private:
int x, y;
}
class Circle{
private:
double radius;
Point center; //成员对象
}
构造:
构造顺序:成员对象的构造顺序与其在类中声明的顺序一致,而非初始化列表中的顺序。
依旧,如果初始化列表中没有对成员对象初始化则会自动调用默认构造,Circle(int r,const Point&x):radius(r){center = x;}中会调用一次Point的默认构造,语句center = x是一条赋值语句,而非复制构造,因为已经默认构造了center
Circle类内部无法调用Point center的private和protected的成员
Circle(double r,int x,int y):radius(r),center(x,y){}
Circle(double r,Point p):radius(r),center(p){}
继承
在已有类的基础上建立新类
派生类的组成:基类的成员,派生类新增成员,派生类改造基类
派生类中可以定义与基类同名的成员变量和成员函数,会自动隐藏基类成员
protected成员:在本类和派生类中能直接访问
class A : public/protected/private B, public/protected/private C{
//...
};
A为派生类,B为基类
A类中可以访问B的protected和public成员,但不可访问private成员
A类外:
- public继承,则B的protected和public成员依旧为A的protected和public成员
- protected继承,基类的 public 成员在派生类中变为 protected,因此无法在类外部通过派生类对象调用,但可以在派生类的子类中调用
- private继承,基类的 public 和 protected 成员在派生类中变为 private,因此无法在类外部通过派生类对象调用,且无法在派生类的子类中调用

派生类的构造函数
创建派生类对象时调用基类的构造函数来初始化基类数据
派生列中的从基类中继承的数据由基类构造函数进行初始化,新增数据由构造函数进行初始化
构造函数的执行顺序:调用基类的构造函数(如有多个基类,则按照它们被继承的顺序依次调用)-> 调用内嵌对象的构造函数 -> 派生类的构造
析构顺序与构造顺序严格相反
Base 中包含int b
class Derived : public Base{
paivate:
int d;
public:
Derived(int bb, int dd) : Base(b)/*这里必须是基类的名称而非成员赋值*/, d(dd) {}
}
派生类的自动初始化列表中无论是否显式写出基类初始化都会调用构造函数,Derived(int bb, int dd) : d(dd) {}会自动调用Base的默认构造函数
继承中如果出现同名成员
- 多继承两个基类有同名成员
class Student{
string name;
}
class Teacher{
string name;
}
class A : public Student, public Teacher{}
用::来区分,Student::name Teacher::name
2. 基类和派生类有同名成员
class A{
int id;
}
class B:public A{
int id
}
基类的成员隐藏,B类调用id默认为class B中成员,B想要访问A中的id需要使用 A::id

这是 C++ 多继承中的菱形继承(钻石继承)问题,关键在于 Assistant 继承 Student 和 Teacher,而 Student、Teacher 又都继承自 Person,导致 Assistant 中存在两份 Person 基类的 id 成员,直接用 Person::id 会产生二义性(编译器无法确定访问的是从 Student 继承的 Person 部分,还是从 Teacher 继承的 Person 部分 )。
基类与派生列的转化(类族的赋值兼容)




多态
不同的类可以有同名的方法,具体实现和结果可以各不相同
包括静态多态和动态多态
静态多态(编译时多态)
在编译系统时就可以确定要调用哪个函数
实现:函数重载,运算符重载
- 函数重载注意事项:
不能仅靠返回值区分,必须从形式参数上区分
不同参数传递方式也无法区别void func(int v) void func(int&v)
运算符重载:
重载运算符是通过对运算符的重新定义,使得其支持特定数据类型的运算操作。重载运算符是重载函数的特殊情况。
重载运算符分为两种情况,重载为成员函数或非成员函数。
当重载为成员函数时,因为隐含一个指向当前成员的 this 指针作为参数,此时函数的参数个数与运算操作数相比少一个。
而当重载为非成员函数时,函数的参数个数与运算操作数相同。
基本格式:
class Example {
// 成员函数的例子
返回值 operator@(除本身外的参数) { /* ... */ }
};
// 非成员函数的例子
返回值 operator@(所有参与运算的参数) { /* ... */ }
输入>>输出<<重载
cin是istream(输入流)类的对象,cout是ostream(输出流)类的对象
重载输入运算符>>
class Person {
private:
string name; int age;
public:
Person() : age(0) {}
string getName() const { return name; }
int getAge() const { return age; }
// 声明为友元函数
friend istream& operator>>(istream& in, Person& p);
};
// 定义重载的输入运算符
istream& operator>>(istream& in, Person& p) {
cout << "Enter name: "; in >> p.name;
cout << "Enter age: "; in >> p.age;
return in;
}
- 是友元函数
- 返回值是istream& (istream类对象的引用)类型,实现链式调用
插播一条关于返回值为引用的芝士
可以避免拷贝开销:按值返回会触发对象的拷贝构造函数,带来不必要的性能开销。而返回引用可以直接返回对象本身,无需拷贝。 实现链式调用:通过返回引用,可以方便地实现对同一对象的连续操作,即链式调用(obj.func1().func2()) 操作原始数据:可以直接修改原始数据结构,这在重载赋值运算符、下标运算符等场景中尤为常见 绝不可以返回局部变量的引用,因为局部变量在函数结束后会被销毁 如果不希望修改返回的对象,可以返回const引用(如const T&)- 关于in>>p.name
是从输入流对象中读取数据,并将其存储到右侧变量中
如:标准输入流cin是istream类的全局对象,代表标准输入设备(通常是键盘)std::cin >> name; // 从键盘读取字符串到 name
自定义输入流:除了 cin,还可以使用其他 istream 对象(如文件流 ifstream、字符串流 istringstream)
in是输入流参数的名称,可以代表:ⅰstd::cin从键盘输入ⅱ文件流,如ifstream,从文件读取ⅲ其它自定义的istream对象
重载输出运算符<<同理,istream换成ostream
动态多态(运行时多态)
通过虚函数和继承实现
编译器类型指的是‘=’左边的类型,运行期类型指的是‘=’右边的类型。当有继承关系时,可能发生编译期类型和运行期类型不同的情况,即编译期类型是父类类型,运行期类型是子类类型。即:父类引用指向子类对象
多态存在的三个必要条件:继承,方法重写,父类引用指向子类对象
父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了
使用父类做方法的形参和返回值,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,因为重写过了
虚函数
的虚函数的作用是允许在派生类中重新定义与基类同名的函数,通过基类指针或者基类引用来访问这个同名函数
virtual 函数类型 函数名(参数列表)
virtual 具有继承性,派生类重写函数时要求函数名称,函数类型,参数个数和类型与虚函数完全一致,可以加上override关键字
动态多态的实现,需要通过指针和引用(赋值兼容性原则),单纯的赋值实现不了多态
例:
class A{
public:
int a;
A(int a = 0):a(a){}
void print(){
printf("A\n");
}
};
class B:public A{
public:
int b;
B(int a=0,int b=0):A(a),b(b){}
void print(){
printf("B\n");
}
};
注意,print()函数没有virtual关键字,不是虚函数,此时在主函数中:
int main(){
A a(1); B b(2,3);
A* x = &b;
x->print();
return 0;
}
会输出A
如果在类A中加入关键字virtual:
class A{
public:
int a;
A(int a = 0):a(a){}
virtual void print(){
printf("A\n");
}
};
主函数不变,此时输出B
有了virtual,在会进行动态绑定,运行时主函数x指针会判断具体类型是A还是B来决定print的执行
如果没有virtual,编译器会默认输出A类型的print,因为x就是A类型的指针,是静态的
虚析构函数
构造函数不可以是虚函数,析构函数可以是
用于指引delete运算符正确析构动态对象
习惯把析构函数声明为虚函数,即使基类并不需要析构函数,以确保撤销动态存储空间时能够得到正确的处理。
#include <iostream>
using namespace std;
// 基类:普通析构函数(非虚)
class Base1 {
public:
~Base1() { cout << "Base1::~Base1()" << endl; }
};
// 派生类
class Derived1 : public Base1 {
private:
int* data;
public:
Derived1() { data = new int[10]; }
~Derived1() {
delete[] data;
cout << "Derived1::~Derived1()" << endl;
}
};
// 基类:虚析构函数
class Base2 {
public:
virtual ~Base2() { cout << "Base2::~Base2()" << endl; }
};
// 派生类
class Derived2 : public Base2 {
private:
int* data;
public:
Derived2() { data = new int[10]; }
~Derived2() {
delete[] data;
cout << "Derived2::~Derived2()" << endl;
}
};
int main() {
// 示例1:普通析构函数(内存泄漏)
Base1* ptr1 = new Derived1();
delete ptr1; // 仅调用 Base1::~Base1(),Derived1 的 data 未释放
cout << endl;
// 示例2:虚析构函数(正确释放)
Base2* ptr2 = new Derived2();
delete ptr2; // 先调用 Derived2::~Derived2(),再调用 Base2::~Base2()
return 0;
}
若类作为基类且包含虚函数,其析构函数必须声明为virtual。
纯虚函数和抽象类
在许多情况下,在基类中不能给出有意义的虚函数定义,这时可把它说明成纯虚函数,把它的定义留给派生类来做。
class 类名{
virtual 返回值类型 函数名(参数表) = 0;
}
纯虚函数无函数体,最后的“=0”不代表返回值为0,这只是一个声明语句
抽象类:包含纯虚函数的类
抽象类不能用于直接创建对象实例,必须做派生类的基类,可以声明抽象类的指针
派生类中应重写基类中的纯虚函数,否则派生类仍将被看作一个抽象类。
shape2D x;
shape2D p;
shape2D f()抽象类不能做返回类型
void f(shape)抽象类不能做参数类型
shapeh (shape &) 引用可以


浙公网安备 33010602011771号