类
成员函数
可以理解为是类当中的函数,称为成员函数
this指针
this是一个额外的隐式参数,它隐式地指向调用该函数的对象的成员
因此当调用成员函数的时候,都会传入该this指针,来表明当前对象是哪一个
const成员函数
在函数名后跟一个const代表的是 const成员函数
int return_id() const{return id;}
这里的const是修改隐式指针this的类型
默认情况下,this的类型是指向类类型非常量版本的常量指针
相当于
Student *const
如果我们的成员函数不会去修改成员,我们就可以把this设置成一个指向常量的指针来提高函数的灵活性
但我们直接声明const this是不合语法的。C++允许我们在函数名后加入一个const来表示这是一个常量成员函数
另外常量对象,常量对象的引用或指针只能调用常量成员函数
类作用域和成员函数
类本身就是作用域,因此我们只要在其作用域内定义函数都算这个类的成员函数
写法如下
返回类型 类名::成员函数名{
.......
}
class Student{
private:
int id;
public:
Student(int i){ id = i;
}
int return_id() const{return id;}
void show_id();
};
void Student::show_id(){
cout<<id<<endl;
}
定义一个返回this对象的函数
函数结尾加上一个
return *this
代表返回调用该函数的对象
示例,这里将两个学生的id加起来
Student& Student::combine(const Student &s1){
id += s1.id;
return *this; // 表示返回该对象
}
我们可以这么理解
前面讲过this返回的是调用该成员函数的对象的指针
*this 对这个指针进行解引用,得到了调用函数的对象
然后注意我们定义的函数类型,它返回的是一个引用,因此我们返回值就被赋给了一个引用。
定义类相关的非成员函数
即然是非成员函数,那就不能定义在类里面
通常我们使用 类对象的引用 去构造一个非成员函数
构造函数
相当于python中的
def __init__(self, xxxxx):
xxxxxx
它是对对象进行一个初始化
当我们没有编写构造函数的时候,编译器会自动给出一个默认构造函数
如果存在类内初始值,则用它来初始化成员
如果没有,则默认初始化成员
某些类不适合默认构造函数初始化
1.大多数类都需要我们自己去控制初始化的值,常常都需要自己去定义一个构造函数
2.如果类当中用到复合类型,比如数组和指针,它们默认初始化后,值是未定义的,因此会引发一系列错误
3.如果类中包含一个其他类的成员,那么默认构造函数无法完成初始化
=default
我们自己定义了构造函数后,那么系统就不会自动帮我们生成一个默认的构造函数。那如果我们对一些比较简单的操作,只需要默认初始化的时候,则需要添加一个构造函数,告诉编译器可以用默认构造函数
Student() = default;
然后我们调用的时候就不用加括号来传递参数了
Student s2;
s2.show_id();
访问控制与封装
定义成public的,在整个程序中都可以被访问
定义成private的,只能被类的成员函数访问
友元函数
友元函数不是类的成员函数
关键字friend
友元函数能直接访问到类中的private成员
但这一定程度上破坏了类的封装性
声明的时候在类里面一定要加上关键字friend
但在类外面去编写友元函数就不需要带上friend
类的其他特性
类的成员函数可以作为内联函数
同样是在声明的时候加上关键字inline
也可以在类的外部使用inline关键字进行定义
成员函数重载
只要函数之间参数数量/类型上不一样即可实现重载
可变数据成员
我们希望修改类中的某个数据成员
即使是在const函数内,也可以达到修改的目的
关键字:mutable
class Student{
private:
mutable int id = 0;
public:
Student(int i){ id = i;
}
Student() = default;
void change_id(int i) const{id = i;}
};
这里我们的change_id 有个const修饰,理论上只能对常量进行操作
但是我们将之前的属性id添加关键字mutable,实现了可修改,若把mutable去掉 则会报错
类数据成员的初始值
class School{
private:
vector<Student> students{Student(12), Student(23)};
};
这里我们定义了一个School类,students是一个由student类组成的vector
当我们提供一个类内初始值时,必须以符号=或者花括号表示
基于const的重载
前面我们提到过const修饰的成员函数,只能对常量进行操作
因此常量对象对应于常量成员函数, 非常量对象对应于非常量成员函数
从而实现了重载
类类型
每个类都是一个唯一的类型
即使两个类的成员列表完全一致,也是不同的类型。
对于一个类来说,它的成员和其他任何类的成员都不是一回事儿
类声明
跟函数一样,类也可以先声明,暂时不定义
class Screen;
7.3.3节练习
class X;
class Y;
class X{
Y* p;
};
class Y{
X q;
};
友元深入理解
除了能将自己类的某些函数定义我友元函数
我们还可以将其他类定义为友元,也可以把以及定义过的类的成员函数作为友元
class S{
friend class clearS;
};
我们定义了类S,其中包含一个友元类clearS。
这样友元类的成员函数都能访问到类S的私有成员
class SetStudent;
class Student{
private:
mutable int id = 0;
public:
Student(int i){ id = i;
}
Student() = default;
friend class SetStudent;
int return_id() const{return id;}
void show_id();
void change_id(int i) const{id = i;}
Student& combine(const Student &s1);
};
void Student::show_id(){
cout<<id<<endl;
}
Student& Student::combine(const Student &s1){
id += s1.id;
return *this; // 表示返回该对象
}
class School{
private:
vector<Student> students{Student(12), Student(23)};
};
class SetStudent{
public:
void set_id(Student &s1){
s1.id = 12;
}
}
一开始我们先对SetStudent 类声明
然后在Student内部引进友元类, 从而SetStudent类能直接访问到Student 的id属性
令类的成员函数作为友元函数
跟之前友元函数类似,但这次我们想要让另外一个类的函数是该类的成员函数,需要在前面说明 类名::函数名
class S{
friend void W::clear();
}
这里就将类W的clear函数声明为类S的友元函数,因此能访问到类S的私有成员
如果想把重载函数声明成友元函数
我们也要分别对其声明
友元声明和作用域
友元函数可以定义在类的内部
但即使是定义好了,我们也不能在类里面的成员函数直接去调用
而是要在外面先进行声明
类的作用域
类的定义分两步
- 首先编译成员的声明
- 直到类全部可见后才编译函数体
名字查找的过程
- 现在名字所在的块,寻找声明语句
- 若找不到,则到外层作用域去寻找
- 还找不到就直接报错
类型名特殊处理
如果类中的成员使用了外层作用域中的名字,而且这个名字代表类型
如
typedef double MONEY;
其中MONEY代表double类型
则类不能在之后定义该名字
所以,通常类型名的定义出现在类的开始处,这样才能确保所有使用该类型的成员都出现在类名的定义之后
成员定义中的普通块作用域的查找
- 首先在成员函数内查找该名字的声明,
- 如果成员函数内没找到,则到类内继续查找
- 如果类内没找到,则在成员函数定义之前的作用域继续查找
最好的写法就是不要把成员名字作为参数或其他局部变量使用
构造函数
构造函数初始值列表
大部分情况下,数据成员的初始化和赋值是没有太大差别的
当数据成员是const或引用,必须将其初始化
如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表来对其初始化
例子
class ConstRef{
private:
int i;
const int ci;
const int c2i;
public:
ConstRef(int ii):ci(ii), c2i(ii){
i = ii;
}
};
用一个分号来表示初始值列表,并用逗号将相邻的数据成员隔开
成员初始化顺序
按照在类定义中的初始顺序
上面那个例子初始化顺序就是 i ci c2i
写构造函数的数据成员顺序最好是与你类定义成员的顺序一致
当然我们也可以使用默认实参来进行初始化
委托构造函数
委托构造函数就是将自己一部分任务交给其他构造函数来完成
示例:
class A{
private:
int a;
int b;
int c;
public:
A(int a1, int b1, int c1){
a = a1;
b = b1;
c = c1;
}
A(): A(1, 2, 3){}
A(int a2): A(a2, 2, 3){}
};
这里我们定义了一个类A,它第一个构造函数传入三个int值,来初始化数据成员a b c
后面两个构造函数则是委托构造函数,第二个构造函数将自己的任务委托到第一个构造函数,并传入初始值1,2,3进行初始化
第三个构造函数,需要传入参数a2,同样它将自己的任务委托给第一个构造函数,用a2, 2, 3进行初始化
它的写法就是
类名(参数): 需要委托的构造函数(对应参数表){}
默认构造函数作用
在某些情况下,我们的数据成员会出现默认初始化
因此最好是,定义了其他构造函数的同时,也要记得定义一个默认构造函数
使用默认构造函数实例化一个对象的时候,调用时不需要加括号
A obj; // 这是一个类A的对象
A obj(); // 这是一个函数
隐式的类类型转换
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型和类类型隐式转换的规则
class Sales_Data{
private:
int a = 2;
string str = "zzk";
public:
Sales_Data(int i){a = i;}
Sales_Data(string s1){str = s1;}
void show_data(Sales_Data sa1){
cout<<sa1.a + a;
}
};
Sales_Data sa1(1);
string str1("asdasjdioj");
sa1.show_data(str1);
首先我们类Sales_Data有两个构造函数,分别是通过int,string来进行构造
show_data成员函数,是将一个对象sa1的属性a和当前对象的属性a加起来并打印
一般来说我们调用show_data的时候是要传入一个对象的
但是我们因为有一个基于字符串string的构造函数
所以调用show_data的时候传入字符串,它默认会调用使用string的构造函数,生成一个对象,再执行接下来的过程。这就是隐式转换
只允许一步转换
编译器关于类型转换只会帮我们自动转换一次
因此,我们隐式转换调用基于string的构造函数,这就已经是一次 类型转换了
如果我们直接
sa1.show_data("dasdasdsa");
类类型转换不总是有效
抑制构造函数的隐式转换
在构造函数前面加上关键字: explicit
注意只能在类内声明加上,在类外部定义时无需重复
并且只能用于直接初始化
sales_data s1("dasd"); // 直接初始化
string str2("dasda");
sales_data s2 = str2; // 拷贝初始化 不能用于有explicit关键字的构造函数
进行显式转换
编译器不会将explicit 用于隐式转换
但我们可以显式地指定它
static_cast<Sales_data>(int i);
使用static_cast
聚合类
当类满足下面条件
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
它有特殊的初始化方法
我们提供一个花括号构成的成员初始值列表来进行初始化
class B{
public:
int a;
string s1;
};
string str1 = "zzjk";
int i = 5;
B a1 = {i, str1};
字面值常量类
要求:
- 数据成员都必须是字面值类型
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值, 则内置类型成员的初始值必须是一条常量表达式,或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
- 类必须使用析构函数的默认定义, 该成员负责销毁对象
类的静态成员
通常来说,每个类的成员都是与其自身对象相关的
但有时候,我们会希望某个成员只与类相关而不是与对象相关
举个例子我们可能写一个银行类,有个数据成员是利率
其他成员函数调用过程中都会使用到利率
而利率一旦出现变化, 我们会希望所有对象的利率都会相应变化, 而不是人一个个每个对象的利率进行修改。这就需要我们使用 静态成员
我们使用作用域运算符来访问静态成员
class Account{
private:
double amount;
static double interestRate;
static double initRate();
public:
void calculate(){amount += amount*interestRate;}
static double rate(){return interestRate;}
static void rate(double);
};
double r = Account::rate();
虽然静态成员不属于类的某个对象,但我们仍然能通过对象的点操作符,指针(->)来访问静态成员
在类外部定义静态成员不要重复static关键字
静态成员定义
由于静态成员不属于类的对象
因此静态成员的定义并不是通过类的构造函数
我们要在类的外部去定义,且一个静态数据成员只能定义一次
double Account::interestRate = initRate();
静态成员的类内初始化
通常情况,类的静态成员不应该在类的内部初始化
我们可以为静态成员提供const整数类型的类内初始值,要求静态成员必须是字面值常量类型的constexpr
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员
静态成员独立于任何对象,且静态数据成员的类型可以就是它所属的类类型
我们可以使用静态成员作为默认实参