第七章 类
类的基本思想是数据抽象和封装
数据抽象是一种依赖接口和实现分离的编程技术
定义在类内部的函数是隐式的inline函数
7.1 定义抽象数据类型
引入this
正是因为在类的内部有this,我们在类的内部可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点。
this是一个常量指针不可以改变他的地址
类作用域和成员函数
编译器分两步处理类,首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
在类的外部定义成员函数
double Sales_data::avg_price() const
{
;
}
在类的外部定义成员函数,成员函数必须要和类声明里面形参列表以及函数名完全相同,且需要包括类的作用域
7.1.3定义类相关的非成员函数
一般来说,如果非成员函数是类接口的组成部分,则这些函数声明应该与类在同一个文件内
7.1.4 构造函数
在对象被创建的时候就会调用构造函数构造函数是以类名字来命名的,没有返回值,不能是const,而且具有多个。也就是说构造函数是可以重载的。但是必须要保证形参列表不同。
合成的默认构造函数
如果说我们定义的类当中没有定义构造函数,那么编译器会给我们一个构造函数,我们将它称为合成的默认构造函数当然默认构造函数无须任何实参
当然合成的默认构造函数只在我们没有声明构造函数的时候编译器才给我们定义
某些类不能依赖于合成的默认构造函数
只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数
如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数
在类里面,数组和指针被默认初始化,它们的值是未被定义的。
=default的和含义
Sales_data() = dafault;
在C++11中,如果我们需要默认的行为,那么可以通过在参数列表后面写上= default来要求编译器生成构造函数。
7.1.5 拷贝、赋值、和析构
某些类不能依赖于合成的版本
使用vector或者string的类能避免分配和释放内存带来的复杂性。
7.2 访问控制与封装
访问说明符
- 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口
- 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节
使用class或struct关键字
class和struct的区别在于,在第一个访问说明符出现之前,class默认的是private,而struct默认的是public。
7.2.1 友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元
友元的声明只能在类的内部,但是具体出现在类的哪里,就不做限制了。友元不是类的成员,也不受它所在区域访问控制级别的约束
声明友元只用在函数开始加一个friend即可。
friend Sales_data add();
一般来说,最好在类定义开始或者结束前的位置声明友元
封装的益处
封装的两个重要优点:
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码
把数据成员的访问权限设成private还有另外一个好处,这么做能防止由于用户的原因造成数据被破坏
友元的声明
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明
如果我们希望一个类的用户能够调用友元函数,我们应该在声明友元的地方之外再声明一次函数。通常来说这个声明包括在头文件里面
许多编译器并未强制限定友元函数必须在使用之前在类的外部声明
7.3 类的其他特性
这些特性包括,**类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this
关于如何定义并使用类类型及友元类的更多知识。
定义一个类型成员
除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名,由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种;
class Screen{
public:
typedef std::string::size_type pos;
private:
pos cursor = 0;
pos height = 0 , width = 0;
std::string contents;
};
class Screen{
public:
using pos = std::string::size_type;
};
重载成员函数
成员函数也可以重载,只需要保证形参列表不同就行了。
可变数据成员
在一个const成员函数内,可以通过在变量的声明中加入mutable关键字做到这一点(希望修改类中某个数据成员)
一个可变数据成员,永远不会是const,即使它是const对象成员。因此,一个const成员函数可以改变一个可变成员的值
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr;
};
void Screen::some_member() const
{
++access_ctr;
}
7.3.2 返回*this的成员函数
从 const成员函数返回*this
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
基于const的重载
在我们在某个对象上调用display时,该对象是否是const决定了应该调用display的哪个版本
class Screen{
public:
Screen &display(std::ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os);
return *this;
}
private:
void do_display(std::ostream &os)const
{
os << contents;
}
}
建议:对于公共代码使用私有功能函数
- 一个基本的愿望是避免在多处使用同样的代码
- 我们预期随着类的规模发展,display函数有可能变得更加复杂,在这时候,写在一处而非两处的作用就比较明显了
- 我们很可能在开发过程中给do_display函数添加某些调试信息,而这些信息将在代码的最终产品版本中去掉。显然,只在一处添加或者删除会比较更容易些
- 这个额外的函数并不会给程序增加任何的开销。因为它隐式地被声明成内联函数
7.3.3 类类型
即使两个类的成员列表完全一致,他们也是不同的类型。对于一个类来说,他的成员和其他任何类(或者任何其他作用域)的成员都不是一回事。
struct First{
int memi;
int getMem();
};
struct Second{
int memi;
int getMem();
};
First obj1;
Second obj2 = obj1; //错误,类型不同
声明一个类对象有两种方式
Sales_data item1;//默认初始化对象 , C++的写法
class Sales_data item1;//等价 C的写法
类的声明
前向声明
class Screen;
这也是个不完全类型
对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明
因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。然而一旦一个类的名字出现后,它就被认为是声明过了(但是尚未定义)因此类允许包含指向它自身类型的引用或者指针
class Link_screen{
Screen window;
Link_screen *next;
Link_screen *prev;
};
7.3.4 友元再探
类之间的友元关系
被声明友元的类,可以在这个类中访问其私有成员,但是必须注意一点,友元关系不可以传递每个类负责控制自己的友元类或友元函数
也就是说,A 是 B的友元类,C是A的友元类,但是C不能访问B的私有成员,也就是说C不是B的友元类。
令成员函数作为友元
class Screen{
/*Window_mgr 必须要在Screen之前声明,并且里面要声明clear函数但不能定义他,声明clear的时候必须得先声明Screen*/
friend void Window_mgr::clear(ScreenIndex);
};
#include <iostream>
using namespace std;
class Nmae;
class getName{
public:
void getname(Name &p);
};
class Name{
public:
double name = 1.1;
friend void getName::getname(Name &p);
private:
string s1 = "hello";
};
void getName::getname(Name &p)
{
cout << p.s1 << endl;
}
int main()
{
// Name name;
// cout << name.name << endl;
getName name1;
Name name22;
// name1.getname(name22);
name1.getname(name22);
return 0;
}
函数重载和友元
对于友元函数来说,函数重载只有对于被声明在类中才可以调用,就算是形参列表不同、函数名相同,没有被声明在类中的重载友元函数还是不能调用该类中的私有成员。
友元声明和作用域
友元本身不一定真的声明在当前作用域中
struct X{
friend void f();
X(){ f(); }//错误
void g();
void h();
};
void X::g(){ return f(); } //错误f()并没有被声明
void f();
void X::h() { return f(); }//正确
7.4类作用域
在类外定义成员函数必须要包括作用域。
7.4.1 名字查找与类作用域
名字查找
- 首先,在名字所在的块中寻找其声明语句,只考虑名字的使用之前出现的声明。
- 如果没有找到,继续查找外层作用域
- 如果最终没有找到匹配的声明,则程序报错
编译器处理完类中的全部声明后才会处理成员函数的定义
用于类成员声明的名字查找
typedef double Money;
string bal;
class Account{
public:
Money blance()
{
return bal;
}
private:
Money bal;
};
找Money找到了typedef,找bal找到了Money bal 而非 string bal
类型名要特殊处理
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名定义之后
typedef double Money;
class Account{
public:
typedef double Money;//重定义,错误
};
成员定义中的普通块作用域的名字查找
- 首先现在成员定义块中查找名字
- 然后在类中查找名字
为了避免成员函数形参和成员变量名字相同作用域的问题,这么有两种解决方式
- 使用this来控制成员变量在成员函数中的使用(不建议)
- 采用两种名字
void Screem::dummy_fcn(pos ht){
cursor = width * height;
}
类作用域之后,在外围的作用域中查找
尽管外层的对象被隐藏掉了,但我们仍然可以用作用域运算符来访问他
void Screen::dummy_fcn(pos height)
{
cursor = width * ::height; //height 是全局的
}
在文件中名字的出现处对其进行解析名字查找
7.5 构造函数再探
7.5.1 构造函数初始值列表
构造函数的初始值有时必不可少
class ConstRef{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii)
{
i = ii;
ci = ii; //错误,不能给const赋值
ri = i; //错误,ri没被初始化
}
//正确打开方式
ConstRef::ConstRef(int ii): i(ii) , ci(ii) , ri(i){ }
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值
建议 使用构造函数初始值,初始化和赋值的区别事关底层效率,前者直接初始化数据成员,后者则先初始化再赋值
成员初始化的顺序
最好令构造函数的初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员
如果可能的话,最好用构造函数的参数作为成员的初始值,而尽量避免同一个对象的其他成员,这样的好处就是我们不必考虑成员的初始化顺序
X(int val): i(val) , j(val){ }
默认实参和构造函数
在构造函数提供默认实参,使用这个实参来初始化成员变量。这就是默认实参构造函数。
7.5.2 委托构造函数
在C++11中有一种委托构造函数
class Student{
public:
Student(string sname , double sscore , int sid):
name(sname) , score(sscore) , id(sid){ }
Student():Student(" ", 0 , 0){ }
Student( const string s): Student(s , 0 , 0 ){ }
private:
string name;
double score;
int id;
};
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行
7.5.4 隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数有时候被我们称为隐式构造函数
抑制构造函数定义的隐式转换
通过将构造函数声明为explicit加以阻止
class Sales_data{
public:
explicit Sales_data(const std::string &s):bookNo(s){ }
};
explicit只在类内定义才有效,且只对一个实参的构造函数有效,且只能用于直接初始化
7.5.5 聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的语法
满足的条件:
- 所有成员都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
struct Data{
int ival;
string s;
};
Data vall = { 0 , "Anna"};//正确
Data vall = { "Anna" , 0};//错误
注意列表初始化必须和成员函数顺序匹配
三个缺点
- 要求所有成员都是public的
- 初始化冗长乏味易出错
- 添加或删除一个成员之后,所有初始化语句都需要更新
constexpr构造函数
构造函数不能是const 但是字面值常量类的构造函数可以是constexpr,且必须要有一个
class Debug{
public:
constexpr Debug(bool b = true): hw(b),io(b),other(b){ }
private:
bool hw;
bool io;
bool other;
};
constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数
7.6 类的静态成员
声明静态成员
在一个类中,不同对象,可能有多种成员变量,但是只有一种静态成员变量
class Account{
public:
void calculate()
{
amount += amount * interestRate;
}
static double rate()
{
return interestRate;
}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
且他们不包含this指针。也不和任何对象绑定在一起。
使用类的静态成员
double r;
r = Account::rate();
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();
和所有的类成员一样,在类外部声明时,必须指明成员所属的类名,static只在类中声明时使用
类的静态成员生命周期伴随程序,且不属于任何一个类的对象。
静态成员的类内初始化
class Student{
public:
static const int id = 1;//不知道为什么改成double就报错 难道double不是constexpr类型?
};
class Student{
public:
static const double id;
};
const double Student::id = 3.14; //可行
一般来说,类的静态成员不属于类的任何一个对象,所以他们并不是在创建类的对象被定义的,这意味着他们不是由类的构造函数初始化的,而且一般来说,我么不能在类的内部初始化静态成员,相反的必须在类的外部定义和初始化每个静态成员
关于类静态成员外部初始化的理解
参考了相关资料,之所以类的静态成员只在类中声明,在外部定义的原因是因为,静态变量定义是唯一的,如果在类中初始化,每次新对象创建静态变量都会被重新初始化,静态变量的意义何在?
静态成员能用于某些场景,而普通成员不能
静态成员可以作为默认实参,而普通成员不行。
写在最后
看完这章才领悟到了C++的复杂性。这里有一个小小的问题
为什么类的静态成员在类中定义整型可以而浮点型就不可以呢?
class Student{
public:
static const int id = 1; //正常
static const double classId = 1.1; //报错
};
留个坑,等以后解决了再回来填。