C++primer第5版 第七章 类
前言
类的基本思想是数据抽象(data abstraction)和封装( encapsulation)。数据抽象是-种依赖于接口(interface)和实现(implementation)分离的编程(以及设计)技术。类的封装则是将实现细节隐藏。
一、定义抽象数据类型
- 定义在内部的函数是隐式的inline函数(函数的实现包括声明和定义)
- 成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this
- 实际上任何定义this的参数都是非法的
- 引入const成员函数
#include <iostream>
#include <string>
using namespace std;
class Sales_data
{
//isbn()函数后面添加const,实际上是修饰this指针,表示
//this是一个指向常量的指针。所以isbn()函数内只能读取不能修改
//this所指向的内容,当然this可以省略.
std::string isbn() const { return this->bookNo; }
private:
std::string bookNo; //图书编号
unsigned units_sold = 0; //销售单价
double revenue = 0.0; //总销售额
};
1、默认情况下this指针是指向类类型没常量,也将导致常量对象不能调用类中的非const的成员函数,
2、同时在const成员函数中也不能调用非const的成员函数。
3、const修饰的成员函数,不能改变对象的成员变量
4、static成员在常量成员函数中可以被改变,因为staitc存在于内存的静态区,和类的普通成员函数位置不一样
- 定义一个返回this对象的函数
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
函数的返回类型决定函数调用是否是左值,返回引用的函数得到的是左值,其他返回类型得到是右值
- 定义类相关的非成员函数
从概念上 属于类的接口组成部分,但不属于类的本身。(函数的声明应该与类在同一个头文件内)
I/O类属于不能被拷贝的类型,只能通过引用来传递他们。
7. 构造函数
1、构造函数的目的是为了给成员变量赋值
2、构造函数可以被重载,没有返回值类型
3、只要对象被创建就会执行默认构造函数
4、编译器会自动帮我定义默认构造函数,但是如果我们定义了有参构造,则编译器将不生成默认构造函数
5 、定义在函数体内的内置类型对象如果没有初始化时,其值时未定义的,类对象如果没有显示地初始化,则其值由类确定
Sales_data() =default;//C++11可以用来生成默认构造函数
6、构造函数初始值列表
Sales_data(std::string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p) { }
std::string isbn() const { return bookNo; }
了解初始化和赋值的区别,初始值列表是初始化,而定义构造函数,函数内赋值是赋值,初始值列表的初始化更加高效。
8.析构、赋值和拷贝
1、析构就如同我们在函数体内创建的局部变量一样,在创建它的块结束时被销毁
2、当类内的成员变量指向堆区数据时,默认的析构函数将不再有效,因此不能使用自动生成的默认析构函数、
二、访问控制与封装
1. 访问控制符来加强类的封装性
public :
成员可以在整个程序内被访问 ,public定义类的接口。
private :
成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
class和struct的区别:
class默认是private,struct默认是public
2. 友元
类允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元。
用friend关键字修饰函数声明即可
friend std::istream& read(std::istream &is, Sales_data &rhs);
1、一般来说,最好在类定义的开始前的位置集中声明友元
2、当把一个成员函数声明为友元时,我们必须明确指出该成员函数属于哪个类
3、重载函数作为友元,尽管名字相同,但是他们依然是不同的函数。要分别对每一个函数进行声明
4、除了类内的声明,一般还要在类外进行声明
封装的优点:
确保用户代码不会无意破坏对象的状态
类的具体实现细节可以随时改变,而无须调整用户级别的代码
三. 类的其他特性
1、令成员作为内联函数
class Screen{
public:
char get()const {return s ;}//直接内类实现
inline char get(pos ht ,pos wd )const;//类内被定义为内联
Screen &move(pos r, pos c) //能在后续被设为内联
}
2、重载成员函数
只要函数之间在参数的数量和/或类型上有去区别就行
3、可变的数据成员
即使在const成员函数中,依然能够被修改。
定义 mutable
private:
int m = 0;
mutable int i = 0; //可变数据成员
static int k;
//static int k = 0; error:只能定义不能初始化static成员
//const static int k = 0 正确:但是只能是int型, 其他double,string都不行。
1、静态变量时所有类的实例化对象所共用的,存在与全局区。
2、普通 的static成员不能让类中的函数访问,且需要在类中访问。
3、static成员只能被static函数访问
4.、const static 成员变量可以在类内定义、
总结: 说完了
*4、返回 this 的成员函数
1、返回引用的函数是左值,是对象本身而非对象的副本,如果返回的不是引用则只能改变临时副本
*从const成员函数返回 this ,那么它的返回值类型将是常量的引用,则不能通过它调用非const的成员函数
2、普通的成员函数是不能通过const进行重载的,由于成员函数隐含了this指针,所以当修饰成为函数为const时,相当于用const 修饰了this指针,传入的类型发生了改变
3、this指针的好处
减少了类对象的拷贝
5、类的声明
类的声明可以和类的定义分开
6、友元
普通函数、类、类的成员函数都可以作为友元函数,同时每个类负责控制自己的友元类或友元函数
四、类的作用域
-
返回值类型的声明
<1. 一个类就是一个作用域。这能够很好的解释为什么当我们在类的外部定义成员函数时必须同时提供类名和函数名。
<2. 返回类型或者任何一个参数类型是类中的,都要指明它属于哪个类。
❤️ 编译器处理完类中全部的声明参会处理成员函数的定义,因为成员函数体在整个类可见后才会处理,所以他能使用类中定义的任何名字
<4. 可以通过::来访问外部作用域被隐藏的变量。!不过我们应该尽量保证不要隐藏外部作用域可能用到的名字。
2、名字查找与类的作用域
<1.首先,在名字所在的块中寻找其声明语句,只考虑在名字使用之前出现的声明
<2.如果没找到,继续查找外层作用域
3.如果最终没有找到匹配的报错
类的定义分两步处理
<1.首先编译成员的声明
<2.直到类全部可见后才编译函数体。
类型名要特殊处理
内层作用域可以重新定义外层作用域的名字,但不能重新定义外层作用域的类型
class Screen{
public :
void dummy_fun(string height)
{
cursor = width * height;//搞不清用哪个了
}
private:
string cursor;
string height = 0 ,width =0
}
五、再探构造函数
-
构造函数初始化列表和构造函数实现函数体内赋值的区别
如果成员是 const 或者是 引用, 必须将其初始化。函数体内赋值会报错 -
成员初始化顺序
成员的初始化顺序与他们在类中定义的顺序一样 -
默认构造和默认实参构造
由于我们提高的默认实参,所以当创建对象没有提供实参时也能调用,实际上就相当于默认构造函数。
不能两个构造函数都使用默认实参,会产生二义性。 -
c++11 委托构造函数
class Sales_data
{
public:
Sales_data(std::string bn, unsigned us, double re): bookNo(bn), units_sold(us), revenue(re)
{ std::cout << "委托Sales_data(std::string bn, unsigned us, double us) \n执行函数体" << "\n"<< std::endl;}
Sales_data(): Sales_data("", 0, 0) //委托三参版本
{ std::cout << "归还Sales_data():Sales_data("", 0, 0) \n执行函数体" << "\n" << std::endl; }
Sales_data(std::string s): Sales_data(s, 0, 0) //委托三参版本
{ std::cout << "归还Sales_data(std::string):Sales_data(s, 0, 0) \n执行函数体" << "\n" << std::endl;}
Sales_data(std::istream &is) : Sales_data() //委托默认版本,默认版本在委托三参版本。
{
read(is, *this);
std::cout << "执行Sales_data(std::istream &is) \n执行函数体" << "\n" << std::endl;
}
}
其他构造函数都委托第一个构造函数进行构造。
- 定义了一个似对象又似函数
Sales_data obj(); //函数
Sales_data obj2; //对象
6.隐式类型转换
如果我们构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,我们也把这种称作转换构造函数。
只允许一步转型
item.combine("99999")
这是错误的,他只允许一次转型,这个需要先将const char * 转成string 然后再转成对象。
7、 抑制构造函数定义的隐式转换
将构造函数声明为explicit,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能执行影视转换,英雌explicit构造函数只能用于直接初始化。
如果我们还需要转换时,可以用显示转换。
item.combine(Sales_data(null_book));
或者
item.combine(static_cast<Sales_data>(cin));
- const和constexpr的区别
为了解决const双重语义的问题,C++11引入constexpr,来承担const常量的那一部分,const只承担只读的作用。
什么是常量:常量要求再编译阶段就能知道其值。
什么是只读:要求在编译过程中不能改变气质。
也就是说当const修饰变量表示常量时,只能表示不能通过该变量改变其值,但是其他指向这个位置的却可以改变
六、类的静态成员
静态成员可以是public或者private的。静态数据成员的类型可以使常量、引用、指针、类类型等。
类的静态成员存在于任何对象之外,对象中不包含任何与静态成员有关的数据。
类似的,静态成员函数也不与任何对象绑定,它们不包含 this 指针。静态成员函数不能声明成 const 的,而且我们也不能在static函数体内使用 this 指针。
1.静态成员函数只可以访问静态成员变量和和静态成员函数。
2.非静态成员函数也可以访问静态成员变量和和静态成员函数。
3.静态成员函数没有this指针,无法访问属于类对象的非静态成员变量和非静态成员函数。
4.由于没有this指针的额外开销,因此静态成员函数与类的非静态成员函数相比速度上会有少许的增长。
5.静态成员函数/变量属于整个类,没有this指针,该类的所有对象共享这些静态成员函数/变量。
6.非静态成员函数/变量属于类的具体的对象,this是缺省的。
7.静态成员变量在类内声明,且必须带static关键字;在类外初始化,且不能带static关键字。
8.静态成员通常在类外初始化,但是如果声明constexpr字面值常量时,可以考虑在类内初始化,但是通常也需要在类外声明