1.类的基本思想:数据抽象和封装
- 数据抽象:一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
- 封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节
2.关于类:
- 定义在类内部的函数是隐式的inline函数(内联函数)
- this:当我们调用成员函数时,实际上实在替某个对象调用它;任何对类成员的直接访问都被看作this的隐式调用
- const成员函数(const紧跟在参数列表后面):表示this是一个常量指针
- 如果非成员函数是类接口的一部分,则这些函数的声明应该与类在同一个头文件中
3.构造函数:初始化类对象的数据成员,类的对象被创建,则会执行构造函数
- 合成的默认构造函数:由编译器创建的构造函数。(1)如果存在类内的初始值,用它来初始化成员;(2)否则默认初始化成员
- 某些类不能依赖于合成的默认构造函数:Ⅰ、只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数;Ⅱ、如果类包含有内置类型或者复合类型的成员。只有当这些成员全部被赋予初始值时,才适合用合成的ⅡⅠ、某些情况下编译器不能为类提供默认的构造函数
- =default:定义一个默认构造函数
4.访问控制与封装:
- class与struct关键字:
/*
在C++中,struct与class类似,既可以包含成员变量,又可以包含成员函数
C++中的struct与class基本是通用的,只有几个细节不同:
1:使用class时,类中的成员默认都是private属性的;而使用struct时,结构体中的成员默认都是public属性的
2.class继承默认是private继承,而struct继承默认是public继承
3.class可以使用模板,而struct不能
*/
/*
1:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,默认的成员变量访问控制是private的
2:当你觉得你要做的更像是一种数据结构时,那么用struct,如果你要做的更像是一种对象时,那么用class
3:然而对于访问控制,应该在程序中明确的指出,而不是依靠默认
*/
- 友元:类可以允许其他类或其他函数访问它的非公有成员,则令其他类或其他函数成为它的友元
- 一般来说,最好在类定义开始或结束前的位置集中声明友元
//定义友元使用friend关键字
class Sales_data{
//为Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
/*......*/
public:
/*.....*/
private:
/*.....*/
};
- 封装的两个重要的优点:(1)确保用户代码不会无意间破坏封装对象的状态;(2)被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码
5.类的其他特性:
- 可变数据成员:在变量的声明中加入mutable关键字
class Screen{
public:
void some_number() const; //即使在一个const对象内也能被修改
private:
mutable size_t access_ctr;
};
void Screen::some_number() const
{
++access_ctr;
}
- 返回*this的成员函数:对于此类成员函数,如果定义的返回类型不是引用,则成员函数的返回值将是*this的副本,因此调用此类成员函数只能改变其副本,而不能更改其对象的值
- 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
- 对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明
- ~友元关系不存在传递性;~每个类负责控制自己的友元类或友元函数;~令某个类的成员函数作为自己的友元,必须声明该成员函数属于哪个类
6.名字查找与类的作用域:
- 编译器处理完类中的全部声明后才会处理成员函数的定义
- 对类成员声明的名字查找:现在类作用域中寻找,若无匹配成员,则在类的外层作用域中寻找
- 类型名的定义通常出现在类的开始处,且不能在类中重新定义
7.构造函数再探:
- 如果成员是const、引用、或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初始值
- 最好令构造函数初始值的顺序与成员声明的顺序保持一致,尽量避免使用某些成员初始化其他成员
class X{
int i;
int j;
public:
//未定义的,i在j之前被初始化
X(int val):j(val),i(j) {}
};
- 委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程
- 在实际中,如果定义了其他构造函数,那么最好提供一个默认构造函数
- 隐式的类类型转换:
//1.转换构造函数:构造函数只接受一个实参,实际上定义了转换为此类类型的隐式转换机制
string null_book = "9-999-99999-9";
item.combine(null_book); //string类型隐式转换为Sales_data类型
//2.只允许一步类类型转换
item.combine("9-999-99999-9"); //错误:需要两步类型转换
//显示的转换成string,隐式的转换成Sales_data
item.combine(string("9-999-99999-9"));
//隐式的转换成string,显示的转换成Sales_data
item.combine(Sales_data("9-999-99999-9"));
//3.抑制构造函数的隐式转换,将构造函数声明成explicit
// 关键字explicit只对一个实参的构造函数有效;只能在类内声明构造函数添加关键字,在类外定义时不允许添加
explicit Sales_data::Sales_data(istream &is) //错误
{
read(is, *this);
}
//explicit构造函数只能用于直接初始化
Sales_data item(null_book); //直接初始化
Sales_data item = null_book; //错误:不能用于拷贝形式的初始化
//4.为转换显示的使用构造函数:使用static_cast
8.聚合类和字面值常量类:
- 聚合类:用户可以直接访问成员、且具有独特的初始化语法形式
//聚合类的特点:
/*
1.所有成员都是public的
2.没有定义任何构造函数
3.没有类内初始值
4.没有基类,也没有virtual函数
*/
//例:
struct Data{
int ival;
string s;
};
- 字面值常量类:数据成员都是字面值类型的聚合类(还有其他几个特例)
- constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式
9.类的静态成员:
- 声明静态成员:在成员的声明之前加上关键字static;静态成员不能被声明成const的
- 定义并初始化一个静态成员:需要指定对象的类型名、类名、作用域运算符以及成员自己的名字
- 如果在类的内部提供了一个初始值,则在外部成员的定义则不需要指定初始值了
- 静态成员可以是不完全类型;可以使用静态成员作为默认实参
10.部分习题解答:
7.8:
read函数需要对传入的对象进行修改
7.10:
同时判断两次读操作的对象
7.13:
Sales_data total(cin);
if (cin)
{
Sales_data trans(cin);
do
{
if (total.isbn() == trans.isbn())
{
total.combine(trans);
}
else
{
print(cout, total);
total = trans;
}
} while (read(cin, trans));
print(cout, total);
}
else
{
cerr << "NO data?!" << endl;
return -1;
}
7.18:
封装实现了类的接口和实现的分离,隐藏了类的实现细节,用户只能接触到类的接口。
优点:隐藏类的实现细节;
让使用者只能通过程序规定的方法来访问数据;
可以方便的加入存取控制语句,限制不合理操作;
类自身的安全性提升,只能被访问不能被修改;
类的细节可以随时改变,不需要修改用户级别的代码;
7.20:
优点:可以灵活的实现访问类的私有成员
缺点:一个类将对非公有成员的访问权授予其他类或函数
7.25:
能,因为Screen的数据成员都是内置类型
7.28:
如使用Screen,则这些函数只返回一个临时副本,不会改变myScreen的值
7.30:
优点:(1)当需要将一个对象作为整体引用而不是引用对象的一个成员时,使用this,则该函数返回对调用该函数的对象的引用
(2)可以非常明确地指出访问的是调用该函数的对象的成员,且可以在成员函数中使用与数据成员同名的形参
缺点:不必要使用,代码多余
7.31:
class X;
class Y{
X object;
};
class X{
Y *my_pointer = NULL;
};
7.33:
pos在类中声明在作用域外使用时要声明属于哪个类
7.34:
出现错误,pos未定义
7.35:
Type有两种类型,应该将最后一个函数的Type声明成Exercise::作用域,因为返回值是double类型
//应添加Exercise::
Exercise::Type Exercise::setVal(Type parm)
{
val = parm + initVal();
return val;
}
7.38:
Sales_data(istream &is = cin) { read(is, *this); }
7.39:
不合法,如果为两个默认构造函数都赋予默认实参,则这两个构造函数都具有了默认构造函数的作用。一旦不提供任何实参地创建类的对象,则编译器无法判断这两个构造函数哪个更好,从而出现二义性错误
7.41:
理解委托构造函数:
#include <iostream>
#include <string>
using namespace std;
class Sales_data
{
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
public:
Sales_data(const string &book, unsigned num, double sellp, double salep) : bookNo(book), units_sold(num), sellingprice(sellp), saleprice(salep)
{
if (sellingprice)
discount = saleprice / sellingprice;
cout << "The constructor can receive booNo, units_sold, sellingprice, saleprice." << endl;
}
Sales_data() : Sales_data("", 0, 0, 0)
{
cout << "The constructor needs no information." << endl;
}
Sales_data(const string &book) : Sales_data(book, 0, 0, 0)
{
cout << "The constructor receives bookNo." << endl;
}
Sales_data(istream &is) : Sales_data()
{
read(is, *this);
cout << "The constructor receives the information that user inputs." << endl;
}
private:
string bookNo;
unsigned units_sold = 0;
double sellingprice = 0.0;
double saleprice = 0.0;
double discount = 0.0;
};
istream &read(istream &is, Sales_data &item)
{
is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.bookNo << " " << item.units_sold << " " << item.sellingprice << " " << item.saleprice << " " << item.discount;
return os;
}
int main()
{
Sales_data first("978-121-15535-2", 85, 128, 109);
Sales_data second;
Sales_data third("978-121-15535-2");
Sales_data last(cin);
return 0;
}
7.43:
#include <iostream>
using namespace std;
class Nodefault
{
public:
Nodefault(int i)
{
val = i;
}
int val;
};
class C
{
public:
Nodefault nd;
C(int i = 5) : nd(i) { }
};
int main()
{
C c;
cout << c.nd.val << endl;
system("pause");
return 0;
}
7.44:
不合法。vector中每个元素都是NoDefault且执行认初始化,然而在类中并未定义默认构造函数,编译器会报错
7.45:
合法,在C中定义了带参数的默认构造函数
7.47:
应该是explicit的,否则编译器就有可能自动将一个string对象转换成Sales_data对象,显得有点随意
7.50:
Person类有3个构造函数,前两个构造函数接收的参数个数都不是1,所以他们不存在隐式转换的问题,当然也不必指定explicit.
Person类的最后一个构造函数Person(std::istream &is);只接受一个参数,默认情况下它会把读入的数据自动转换成Person对象,可以明确指定。在其他情况下则不希望自动类型转换的发生。所以应该把这个构造函数指定为explicit的。
7.51:
string接受的单参数是const char*类型,如果我们得到了一个常量指针,则把它看做string对象是自然而然的过程,编译器自动把参数类型转换成类类型也非常符合逻辑,因此我们无须指定为explicit。
与string相反,vector接受的单参数是int类型,这个参数的原意是指定vector的容量。如果我们在本来需要vector的地方提供一个int值并且希望这个int值自动转换成vector,则这个过程显得比较牵强,因此把vector的单参数构造函数定义成explicit的更加合理。
7.52:
这个程序相对item进行聚合类初始化操作,用花括号内的值初始化item的数据成员。然而实际过程与程序的愿意不符合,编译器会出错。聚合类要满足没有类内初始值这一条件,因此去掉类内初始值,就可以正常运行了。
7.54:
这些以set_开头的成员不能声明成constexpr,这些函数的作用是设置数据成员的值,而constexpr函数只能包含return语句,不允许执行其他任务。
7.55:
因为Data类是聚合类,所以它也是一个字面值常量。
7.56:什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?
静态成员是指声明语句之前带有关键字static的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。
静态成员的优点包括:可以是私有成员,而全局对象不可以;通过阅读程序可以非常容易地看出静态成员与特定类关联,使得程序的含义清晰明了。
静态成员与普通成员的区别主要体现在普通成员与类的对象关联,是某个具体对象的组成部分;而静态成员从不属于任何具体的对象,它由该类的所有对象共享。静态成员可以作为默认实参,而普通数据成员不可以。
7.58:
在类的内部,rate和vec的初始化是错误的,因为除了静态常量成员之外,其他静态成员不能在类的内部初始化。另外,example.c文件的两条语句也是错误的,在这里必须给出静态成员的初始值。
关于Sales_data类:
//Sales_data.h
struct Sales_data
{
//默认构造函数
Sales_data(){}
//以istream为参数的构造函数,调用read函数以给数据成员赋值
Sales_data(istream &is);
//得到书本的isbn
string isbn() const { return bookNo; }
//声明combine函数
Sales_data& combine(const Sales_data& item);
//声明avg_price函数
double avg_price() const;
//书本的isbn号
string bookNo;
//书本的销售量
unsigned units_sold = 0;
//书本的销售价格
double revenue = 0.0;
};
//实现avg_price函数,得到平均价格,并定义成内联函数
inline
double Sales_data::avg_price() const
{
if (units_sold)
{
return revenue / units_sold;
}
else
{
return 0;
}
}
//实现combine函数,将两个对象的成员相加,返回Sales_data类型对象
Sales_data& Sales_data::combine(const Sales_data& item)
{
units_sold += item.units_sold;
revenue += item.revenue;
return *this;
}
//输入的交易信息包括ISBN、售出总数、售出价格
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = item.units_sold * price;
return is;
}
//输出交易信息包括ISBN、售出总数、售出总额和平均价格
ostream &print(ostream &os,const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();
return os;
}
//实现构造函数(必须定义在类的外部,因为read函数非类成员函数)
Sales_data::Sales_data(istream &is)
{
read(is, *this);//read函数的作用是从is中读取一条交易信息然后存入this对象中
}
关于Person类:
//Person.h
class Person
{
public:
Person(){}
//定义构造函数,对成员初始化
Person(string p_name, string p_address)
{
name = p_name;
address = p_address;
}
//返回姓名
string getname() const{ return name; }
//返回地址
string getaddress() const { return address; }
//定义两个公有成员,姓名、地址
string name, address;
};
//输入的信息包括姓名及地址
istream &read(istream &is, Person &person)
{
is >> person.name >> person.address;
return is;
}
//输出的信息包括姓名及地址
ostream &print(ostream &os, Person &person)
{
os << person.getname() <<" "<< person.getaddress() << endl;
return os;
}
关于Screen类:
class Screen;
class window_mgr
{
public:
using ScreenIndex = vector<Screen>::size_type;
void clear(ScreenIndex);
private:
vector<Screen> screens;
};
class Screen
{
public:
typedef string::size_type pos;
//默认构造函数
Screen() = default;
//构造函数1
Screen(pos ht,pos wd):height(ht),width(wd),contents(ht * wd , ' '){}
//构造函数2
Screen(pos ht, pos wd, char c):height(ht),width(wd),contents(ht * wd,c){}
char get() const { return contents[currsor]; }
char get(pos r, pos c) const { return contents[r * width + c]; }
Screen &move(pos r, pos c);
Screen &set(char c);
Screen &display(ostream &os);
friend void window_mgr::clear(ScreenIndex);
private:
pos currsor = 0;
pos height = 0, width = 0;
string contents;
};
inline
Screen &Screen::move(pos r, pos c)
{
pos row = r * width;
currsor = row + c;
return *this;
}
inline
Screen &Screen::set(char c)
{
contents[currsor] = c;
return *this;
}
inline
Screen &Screen::display(ostream &os)
{
os<< contents;
return *this;
}
void window_mgr::clear(ScreenIndex i)
{
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}