类
类的基本思想:数据抽象和封装
- 数据抽象:一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
- 封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节
- 类
- 定义在类内部的函数是隐式的inline函数(内联函数)
- const成员函数(const紧跟在参数列表后面):表示this是一个常量指针
- 如果非成员函数是类接口的一部分,则这些函数的声明应该与类在同一个头文件中
定义抽象数据类型
构造函数
- 某些类不能依赖于合成的默认构造函数。
- 只有当类没有声明任何构造函数,编译器才会自动生成默认构造函数。
- 如果类包含有内置类型或者复合类型的成员时,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
- =default:显式声明默认构造函数
- const对象在完成构造函数对其初始化之后才真正获取其“常量”属性
- 默认构造函数:提供没有初始值的类成员默认初始化
- 封装是指保护类的成员不被随意访问的能力。通过把类的实现细节设置为private,我们就能完成类的封装,从而实现类的接口和实现的分离。
- 封装的好处:1.确保用户代码不会破坏封装对象的状态;2.被封装的类的具体细节可以随时改变,而无需调整用户级别的代码
- 构造函数的初始值有时必不可少。有时我们可以忽略数据成员初始化和赋值之间的差异,但并不总是这样子。当成员是const或者是引用时就必须初始化。
- 因此建议使用构造函数初始值进行初始化,一方面可以提高效率,一方面可以避免某些意想不到的编译错误
- 类成员初始化顺序和他们在类中出现的顺序一致,尽量让构造函数的初始值顺序同其成员声明顺序保持一致
访问控制与封装
- 在C++中,struct与class类似,既可以包含成员变量,又可以包含成员函数
- C++中的struct与class基本是通用的,只有几个细节不同:
- 使用class时,类中的成员默认都是private属性的;而使用struct时,结构体中的成员默认都是public属性的
- class继承默认是private继承,而struct继承默认是public继承
- class可以使用模板,而struct不能
- 当你觉得你要做的更像是一种数据结构时,那么用struct,如果你要做的更像是一种对象时,那么用class
- 然而对于访问控制,应该在程序中明确的指出,而不是依靠默认
- 友元
- 类可以允许其他类或其他函数访问它的非公有成员,则令其他类或其他函数成为它的友元
- 一般来说,最好在类定义开始或结束前的位置集中声明友元
class Sales_data{
friend Sales_data add(const Sales_data&, const Sales_data&);
/*......*/
public:
/*.....*/
private:
/*.....*/
};
类的其他特性
- 可变数据成员:在变量的声明中加入mutable关键字,即使在一个const对象内也能修改。
class Screen{
public:
void some_number() const;
private:
mutable size_t access_ctr;
};
void Screen::some_number() const
{
++access_ctr;
}
- 返回*this的成员函数:对于此类成员函数,如果定义的返回类型不是引用,则成员函数的返回值将是*this的副本,因此调用此类成员函数只能改变其副本,而不能更改其对象的值
- 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
- 对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明
- 友元关系不存在传递性;每个类负责控制自己的友元类或友元函数;令某个类的成员函数作为自己的友元,必须声明该成员函数属于哪个类
类的作用域
- 一旦遇到类名,定义的剩余部分就在类的作用域之内了,结果就是我们可以直接使用类的成员而无须再次授权了。
- 编译器处理完类中的全部声明后才会处理成员函数的定义。也就是直到类全部可见后才编译函数体。
构造函数再探
隐式类型转换
- 如果成员是const、引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初始值
- 如果构造函数只接受一个实参,那么它实际上定义了转换为此类类型的隐式转换机制,这种构造函数称为转换构造函数
- 编译器只能自动执行一步类型转换
- 可以通过在类内将构造函数声明为explicit阻止隐式类型转换,explicit只对一个参数的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换
- explicit只能出现在类内构造函数声明处,且explicit构造函数只能用于直接初始化。
- 可以为转换显式地使用构造函数,如static_cast。
string null_book = "9-999-99999-9";
item.combine(null_book); //string类型隐式转换为Sales_data类型
// 只允许一步类类型转换
item.combine("9-999-99999-9"); //错误:需要两步类型转换
item.combine(string("9-999-99999-9"));
item.combine(Sales_data("9-999-99999-9"));
explicit Sales_data::Sales_data(istream &is) //错误
{
read(is, *this);
}
//explicit构造函数只能用于直接初始化
Sales_data item(null_book); //直接初始化
Sales_data item = null_book; //错误:不能用于拷贝形式的初始化
委托构造函数
- 使用它所属类的其他构造函数执行它自己的初始化过程
类的静态成员
- 类的静态成员用static标识,使得其与类关联在一起
- 类的静态成员和静态成员函数存在于任何对象之外,对象中不包含任何与静态成员数据有关的数据
- 静态函数不包含this指针,且不能被声明为const
- 通过作用域运算符直接访问静态成员,成员函数不用通过作用域运算符就能直接使用静态成员。
- 可以用类的对象、引用或者指针来访问静态成员
- static必须出现在类中,但static函数可以在类外定义,不能重复static关键字
- 因为静态数据成员不属于类的任何一个对象,也就是说他们不是由类的构造函数初始化的。
- 必须在类的外部定义和初始化每个静态成员。一个静态数据成员只能定义一次。而静态成员函数既可以在类的内部定义也可以在类的外部定义。
- 静态成员的类内初始化:类内可以初始化const或者constexpr的成员
- 静态成员可以作为默认实参
static constexpr int period = 30; //在类内提供初始值
constexpr int Account::period; // 即使一个常量静态数据成员在类内部初始化了,也应该在类的外部定义一下该成员。
部分实现代码:
#include<string>
using std::string;
#include<iostream>
using namespace std;
class Screen {
public:
//类还可以自定义某种类型在类中的别名,可以隐藏Screen实现的细节
typedef string::size_type pos;
//using pos = string::size_type;
Screen() = default;
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c)
{ }
// 读取光标处字符
// 类内成员函数为隐式内联
char get() const
{
return contents[cursor];
}
//显式内联
inline char get(pos ht, pos wd) const;
Screen &move(pos r, pos c);
void some_member() const;
// 返回值为引用说明返回为对象本身而不是对象的拷贝,这样子理解返回值是否为引用好像比较舒服一点
Screen &set(char);
Screen &set(pos, pos, char);
Screen &display(ostream &os)
{
do_display(os);
return *this;
}
//基于const的重载
const Screen &display(ostream &os) const
{
do_display(os);
return *this;
}
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
// mutable关键字标识可变数据成员,即使const成员函数也可以修改
// mutable size_t access_ctr = 0;
//建议对公共代码使用私有功能函数,一般用于const和非const重载版本的一个功能函数
void do_display(ostream &os) const
{
os << contents;
}
};
// 类外定义为内联
inline Screen &Screen::move(pos r, pos c)
{
cursor = r * width + c;
return *this;
}
char Screen::get(pos ht, pos wd) const
{
return contents[ht * width + wd];
}
//void Screen::some_member() const
//{
// //即使const成员函数也可以修改access_ctr的值
// ++access_ctr;
//}
Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
Screen &Screen::set(pos row, pos col, char c)
{
contents[row * width + col] = c;
return *this;
}
#endif
//Sales_data.h
//
// Created by like0 on 2019/2/28.
//
#ifndef CHAPTER7_SALES_DATA_H
#define CHAPTER7_SALES_DATA_H
#include <iostream>
using namespace std;
#include<string>
using std::string;
class Sales_data
{
//所有的成员函数必须在类内部声明,在类外部定义,定义在类内部的函数是隐式的inline函数;定义在外部默认不是内联的
// 友元声明不受访问说明符的约束,一般在类开头或者结尾声明
// 一个类也可作为友元,一个类的成员函数也可以成为另一个类的友元(需要通过作用域标识符说明)
friend Sales_data add(const Sales_data&, const Sales_data&);
friend ostream& print(ostream&, const Sales_data&);
friend istream& read(istream&, Sales_data&);
//C++默认成员属性为private,外部函数无法防伪
//我擦,这个bug找了很久,加了个public就好了
//访问说明符public成员定义类的接口,private部分则封装了类的实现细节
public:
//构造函数
//类可以不提供构造函数,编译器会自动提供默认构造函数
//默认构造函数可以没有参数,也可以通过提供默认参数的形式
//默认构造函数,当定义了其他构造函数,最好也提供一个默认构造函数
//内置类型数据被提供了初始值时默认构造函数才有效
// 类内定义的构造函数
// 冒号和花括号之间的部分:构造函数初始值列表
// 非委托构造函数
Sales_data(const string &s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p*n) {
cout << "all" <<endl;
}
// 默认构造函数,委托三参构造函数
Sales_data() : Sales_data("", 0, 0) {
cout << "default" << endl;
}
Sales_data(const string &s) : Sales_data(s, 0, 0) {
cout << "string" << endl;
}
//委托默认构造函数
Sales_data(istream &is) : Sales_data()
{
read(is, *this);
cout << "istream&" << endl;
}
//Sales_data(const string &s) :
// bookNo(s) { }
// 对Sale_data类的操作
// 所有成员必须在类内声明,但可以在类外定义
// 参数列表之后的const的作用是修改隐式this指针的类型
// 默认情况下this为指向非常量对象的常量指针,意味着不能把this指针绑定到一个常量对象上
// 把this设置为指向常量的指针有助于提高函数的灵活性
// 这种函数叫做常量成员函数
string isbn() const
{
return bookNo;
//bookNo相当于this-> bookNo
}
Sales_data& combine(const Sales_data&);
private:
inline double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//定义在类外部的构造函数
//之前read提示找不到标识符,发现将非成员函数声明为类的友元函数就可以了,这样类成员变量修改为private也没关系
// 经验就是:成员函数调用非成员函数要通过firend声明
// 书上写,如果希望类的用户能够调用某个友元函数,就需要在友元声明之外对函数再次声明一次
// 上面不是必须的,这个和编译器有关系,经验就是把友元函数的声明与类的定义放在同一个头文件中
//Sales_data::Sales_data(istream &is)
//{
// // 从is中读取交易信息存入this对象中
// read(is, *this);
//}
// 定义在类外的成员函数类作用域运算符不可少
double Sales_data::avg_price() const {
if (units_sold)
return revenue/units_sold;
else
return 0;
}
// combine类似于"+"内置运算符,返回类型应该为可以被修改的左值,因此返回值必须为引用
// 调用一个返回引用的函数得到左值,其他返回类型得到右值
// *this解引用得到返回对象
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
//Sales_data的非成员数据接口
//返回值为副本而非引用
Sales_data add(const Sales_data&, const Sales_data&);
//IO类属于不能被拷贝的类型,因此我们只能通过引用来传递他们
//因为读取和写入的操作会改变流的内容,所以两个函数接受的是普通引用
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);
// 类如果有其他的构造函数,也应该定义默认构造函数,因为类在存在构造函数的情况下不会调用默认构造函数
// 这个类没有默认构造函数
class NoDefault {
public:
NoDefault(int i)
{
var = i;
}
int var;
};
class C {
public:
// 类C的默认构造函数
// 由于NoDefault没有默认构造函数,因此只能显式调用NoDefault的带参构造函数初始化nodefault
C(int i = 5) : nodefault(i)
{
}
NoDefault nodefault;
};
// 静态成员相关的类
class Account
{
public:
void calculate()
{
amount += amount * interestRate;
}
static double rate()
{
return interestRate;
}
static void rate(double);
private:
string owner;
double amount;
static double interestRate;
static double initRate();
};
void Account::rate(double newRate)
{
interestRate = newRate;
}
double Account::interestRate = initRate();
int hight = 1;
class SC
{
public:
bool isHightEnough(int height)
{
return height > ::hight; //::表示访问位于类外层的对象,
}
private:
int hight = 0;
int width = 0;
};
#endif //CHAPTER7_SALES_DATA_H
//main.cpp
#include "Sales_data.h"
#include"Screen.h"
#include<iostream>
using std::istream;
using std::ostream;
#include<vector>
using std::vector;
// 输入信息包括ISBN、售出总数和售出价格
istream & read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream& print(ostream& os, const Sales_data& item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " "
<< item.avg_price() << endl;
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
int main() {
// 定义对象时用对象名+括号即可,如果是默认初始化则不用加括号!!!
Sales_data item1;
print(cout, item1);
Sales_data item2("isbn-12-1312", 5, 10.00);
print(cout, item2);
Sales_data item3("isbn-13-3212");
print(cout, item3);
//Sales_data item4(cin);
//print(cout, item4);
// 隐式转换test,没有发现有啥用
string null_book = "isbn-13-3212";
item3.combine(null_book);
print(cout, item3);
vector<Sales_data> Sales_datas{ Sales_data(" ", 0, 0) };
// 错误版本,类内初始值必须以等号或者花括号表示
//vector<Sales_data> Sales_datas(Sales_data(" ", 0, 0));
Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(cout);
cout << endl;
myScreen.display(cout);
cout << endl;
C c;
cout << c.nodefault.var << endl;
// 类的静态成员相关
double r = Account::rate();
//double Account::interestRate = initRate();
return 0;
}