类的基本思想
数据抽象
- 数据抽象是依赖于接口和实现分离的程序技术。
- 类的接口包括用户能执行的操作。
- 类的实现包括类数据成员、负责接口实现函数体和定义类所需的各种私有函数。
封装
- 封装实现了接口和实现的分离,隐藏其实现细节,,类的用户只能使用接口而无法访问实现部分。
抽象数据类型
类是一种抽象数据类型,只能访问他的接口,不能访问它的数据成员。
类的定义
函数定义例程
- 函数成员要在函数内部声明,成员函数体的定义可以在类内也可以在类外。
- 编译器处理类方式:首先编译成员声明,然后到成员函数体。
struct Sales_data{
//成员函数
std::string isbn()const{
return bookNo;
}
Sales_data& combine(const Sales_data&);
double avg_price()const;
//数据成员
std::string bookNo; //书编号
unsigned units_sold = 0; //销量
double revenue = 0.0; //总销售额
}
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
std::istream &print(std::istream&,Sales_data&);
定义类型成员
- 类可以自定义某种类型再类中别名。类型名字也有访问限制。
- 类型成员必须先定义再使用,通常出现在类开始的地方。
- 注:类型名定义最好在类开始处
class Screen {
public:
typedef std::string::size_type pos;
//using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
- 进行类型名查找时 :类全部可见后编译函数体,此时函数体可以使用类中定义的任何名字
- 对于成员函数声明所用类型名:先查找类内该声明前是否存在相关声明,没有再去外层作用域上层查找,都没有报错。
- 对于成员函数定义内所用类型名:类内上下寻找相关声明,没有再去外层作用域上层查找,都没有报错。
//情况1 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型,int,height是内层的hhdy height,int类型。
hhdy balance(){return height;}
private:
//using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况2 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是string类型,height是内层的hhdy height,string类型。
string balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况3 报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型int,height是内层的hhdy height,是string类型,报错。
hhdy balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
定义数据成员
- 可变数据成员:当想对const成员函数内修改某个数据成员,可以加入mutable关键字。
- 对于类内初始值,使用“=”或花括号直接初始化。
定义成员函数
- 类内部成员函数是自动内联的,在类外部的函数需要说明inline。
- 成员函数可以重载
类外部定义成员函数
- 可以在类外部定义成员函数,此时,定义与类中声明必须一致。
- 成员函数可以被重载。
- 类外部定义成员的名字必须包含他所属的类名:
double Sales_data::avg_price()const{
if(units_sold){ //隐式使用类中成员,相当于this->units_sold
return revenue/units_sold;
}
else{
return 0;
}
}
定义类相关的非成员函数
- 指类的辅助函数
- 函数属于类单但不定义在类中时,一般应与类声明在同一个头文件,而不是类定义位置。
- 以I/O流为例,IO类属于不能拷贝的类型,只能通过引用传递它们。读取写入会改变流,所以使用普通引用而非常量引用。
//读
std::istream& read(std::istream&is, Sales_data& item) { //数据读给给定流
double price = 0;
cout << "输入书号:" << endl;
is >> item.bookNo;
cout << "输入价格:" << endl;
is >> price;
cout << "输入卖出数目:" << endl;
is >>item.units_sold;
item.revenue = price * item.units_sold;
return is;
}
//写
std::ostream& print(std::ostream& os, Sales_data& item) { //给定对象打印到给定流
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price(); //print函数不负责换行,由用户决定是否换行。
return os;
}
//使用
read(cin, total);
print(cout, total);
类的静态成员
- 在成员声明之前加上关键字static,使之与类的所有对象关联共享。
- 对于静态成员函数,不能声明为常量类型,不能在静态成员函数中使用this。
定义静态成员
- 对于数据成员,在成员声明前加上static关键字。类型可以是常量、引用、指针、类等。对于定义,静态数据成员定义在任何函数之外,并且必须在类的外部定义和初始化每个静态成员,最好把静态数据成员定义和其他非内联函数放在一起。静态数据成员定义类似全局变量,定义在任何函数之外,被定义后就一直存在于程序的整个生命周期中。
- 对于成员函数,static关键字只出现在类内部的声明中。在外部定义函数时,不加static关键字。
- 类内初始化:如果要对静态成员进行类内初始化,可以采用const整数类型类内初始值,但静态成员必须是字面值常量类型的constexpr。即使常量静态成员在类内部被初始化了,通常也应在类外部定义一下该成员。
- 静态成员可以是不完全类型,可以是所属类类型。可以做默认实参。
class hhdy {
public:
hhdy() = default;
hhdy(int n = hh):xx(n){} //静态成员可以做默认实参
static int display_hh() {
return hh;
}
static void change_hh(int n) {
hh = n;
}
private:
static int hh;
int xx;
static int hh_init(int);
static constexpr int hhnum = 3; //静态字面值常量,类内初始化
static hhdy hhdy1;
hhdy* hhdy2;
};
int hhdy::hh = hh_init(5); //外部定义静态数据对象
constexpr int hhdy::hhnum; //静态字面值常量,类内初始化,最好在类外部也定义一下该成员
int hhdy::hh_init(int n) { //成员函数外部定义,不加static
return n;
}
int main() {
hhdy h1;
hhdy* h2 = &h1;
//static类型三种使用形式
std::cout << hhdy::display_hh() << std::endl;
hhdy::change_hh(9);
std::cout << h1.display_hh() << std::endl;
std::cout << h2->display_hh() << std::endl;
std::cout << hhdy::display_hhnum() << std::endl;
return 0;
}
构造函数
- 类通过一个或几个特殊的成员函数来控制器对象初始化过程。
- 构造函数没有返回类型。
- 构造函数不能声明成const类型
默认构造函数
当对象被默认初始化或值初始化时,自动执行默认构造函数:
- 默认初始化:
不使用初始值定义一个非静态变量或数组;
一个类本身含有类成员,且使用合成默认构造函数
类类型成员采用赋值的方法赋初值
- 值初始化
数组初始化中提供初始值数量少于数组大小
不使用初始值定义局部静态变量
T()类,如vector(3),说明vector中包含3个int类型,执行默认构造函数,值为0。
合成的默认构造函数
- 当类没有显式的定义构造函数,编译器会为我们隐式的定义一个默认构造函数,这种构造函数被称为合成的默认构造函数。
- 合成的默认构造函数初始化方法是,如果初始化类存在类内初始值,则用它初始化成员,否则默认初始化该成员。
- 对于合成的默认构造函数有如下限制:
1.编译器只有在类中没有构造函数时才会生成一个默认的构造函数;
2.合成的默认构造函数可能执行错误操作,比如定义在块中的内置类型或符合类型默认初始化是未定义的。
3.类中包含一个其他类类型的成员且这个成员类型没有默认构造函数时。不能为这个类合成默认构造函数。
定义构造函数
- 如果编译器不支持类内初始值,则所有构造函数都应该显式的初始化每个内置类型的成员
Sales_data() = default; //默认构造函数,=default在内部,函数内联。外部不内联。
Sales_data(const int& n):bookNo(n){}//构造函数初始值列表
Sales_data(const int& n,unsigned m,double p):bookNo(n),units_sold(m),revenue(p*m){}
Sales_data(std::istream&); //声明
//在类外部定义构造函数
Sales_data::Sales_data(std::istream& is) {
read(is, *this);
}
构造函数的初始化与赋值
- 对于const类型与引用必须进行初始化,对于某种类型没有定义默认构造函数时,也必须对该类型成员进行初始化。
//初始化
Sales_data(const int& n,unsigned m,double p):bookNo(n),units_sold(m),revenue(p*m){}
//赋值/拷贝形式初始化:具体操作是先进行默认初始化,再赋值。
Sales_data(const int& n,unsigned m,double p){
bookNo = n;
unit_sold = m;
revenue = p*m;
}
成员初始化顺序
每个成员在初始化中只出现一次;
初始化顺序与在类中定义顺序一致。
class hhdy{
int i;
int j;
public:
hhdy(int val):j(val),i(j){} //报错,使用未初始化j定义i
};
委托构造函数
- 委托构造函数可以使用他所属类的其他构造函数执行自己的初始化过程。
class hhdy {
int height ;
int weight ;
public:
friend void read(std::istream& is, hhdy& item);
friend void print(std::ostream& os, hhdy& item);
//非委托构造函数
hhdy(int a, int b) :height(a), weight(b){}
//委托构造函数
hhdy() :hhdy(160, 90){}
hhdy(int a):hhdy(a,90){}
hhdy(std::istream& is) :hhdy() { read(is, *this); }
};
void read(std::istream& is, hhdy& item) { //数据读给给定流
std::cout << "height:" << std::endl;
is >> item.height;
std::cout << "weight:" << std::endl;
is >> item.weight;
}
void print(std::ostream& os, hhdy& item) {
os << "height:" << item.height<<"weight:"<< item.weight<<std::endl;
}
int main() {
hhdy h1 = hhdy();
hhdy h2 = hhdy(180);
hhdy h3 = hhdy(std::cin);
print(std::cout, h1);
print(std::cout, h2);
print(std::cout, h3);
return 0;
}
转换构造函数:隐式类类型转换
- 当构造函数只接收一个实参时,可以进行此类类型的隐式转换。
- 类型转换只能进行一步
- 函数声明explicit,抑制构造函数定义的隐式转换,此时拷贝形式初始化也失效了。
- 对于标准库:接受单参数的const char*的string构造函数不是explicit类;接受单容量的vector构造函数时explicit类。
class hhdy {
int height = 0;
int weight = 0;
std::string s = "00";
public:
hhdy() = default;
hhdy(std::string s1) :s(s1) {}
explicit hhdy(int n):height(n){}
hhdy& getmember(const hhdy& h) { //隐式转换时必须定义为常量引用,否则输入必须是左值
this->height += h.height;
return *this;
}
};
int main() {
hhdy h1;
hhdy h2(90);
hhdy h3 = 90; //报错,不存在int到hhdy的隐式转换
//“456”类型为char[4]
h1.getmember(hhdy("456")); //显式转换成hhdy类,隐式转换成string类
h1.getmember(std::string("456"));//隐式转换为hhdy类,显式转换为string类
h1.getmember("456");//两步隐式,报错
h1.getmember(90); //报错,不存在int到hhdy的隐式转换
h1.getmember(static_cast<hhdy>(90)); //可以强制转换
return 0;
}
访问控制与封装
访问说明符
- 通过访问说明符可以加强类封装性。public说明符之后的成员可以在整个程序内被访问,public成员定义类的接口。private说明符之后的成员可以被类成员访问,但不能使用该类的代码访问。
- 访问说明符的有效范围到下一次访问说明符或类的结尾为止
- 对于class关键字和struct关键字:struct后默认public;class后默认private。
class Sales_data {
public:
//构造函数
Sales_data() = default;
Sales_data(const int& n):bookNo(n){}
Sales_data(const int& n,unsigned m,double p):bookNo(n),units_sold(m),revenue(p*m){}
Sales_data(std::istream&); //声明
//成员函数
int isbn() const {
return bookNo;
}
void inputisbn(int i) {
bookNo = i;
}
Sales_data& combine(const Sales_data&);
double avg_price()const;
private: //数据成员
int bookNo; //书编号
unsigned units_sold = 0; //销量
double revenue = 0.0; //总销售额
};
对非公有类型访问——友元
- 对于非公有类型的访问:令其他类或者函数成为它的友元。增加一条以friend关键字开始的函数声明语句。
- 最好在类开始或结束位置集中声明友元。
- 友元的声明仅指定了访问权限,而非通常意义上的函数声明。如果希望类的用户能调用某个友元函数,必须在友元声明外再专门对函数进行一次声明。
类之间的友元
- 对于类A要调用类B里的私有成员时,要在类B中声明类A为友元。
- 友元之间没有传递性,如果在类B里定义了类C是友元,类C不是类A的友元。
- 友元是单向的
- 友元类必须在内部调用
class hhdy {
public:
friend class zwhy;
int get(zwhy i) {
return i.height; //报错,不能访问
}
private:
int height = 160;
};
class zwhy {
public:
int get1(hhdy i) {
return i.height;
}
int get2(int i) {
return i
}
private:
int height = 180;
};
int main() {
zwhy xx;
hhdy hh;
std::cout << xx.get1(hh) << std::endl; //调用hhdy类型
std::cout << xx.get2(hh.height) << std::endl; //报错,不可访问
return 0;
}
成员函数的友元
- 可以只将类B中的一个函数声明为类A的友元。所以当把一个成员函数声明为友元时,必须明确指出该成员函数属于哪个类。
成员函数作为友元的步骤(将类B中的一个函数func声明为类A的友元)
1.定义类B,声明但不定义函数func,如果函数声明中用到类A,需要在定义类B前先声明类A
2.定义类A,声明友元类B
3.定义类B的函数func
- 对于重载函数的声明,需要对这组函数的每一个分别声明。
- 类和非成员函数的声明不是必须在他们的友元声明之前。友元声明中出现的名字如果是第一次出现会隐式假定改名字在当前作用域可见。(getweight)
- 友元声明影响访问权限。函数声明调用前必须被声明过。函数调用定义前必须被定义。
class hhdy;
class zwhy {
public:
int get(hhdy);
int get(hhdy,int);
private:
int height = 180;
};
class hhdy {
public:
friend int getweight(hhdy);
friend int zwhy::get(hhdy);
friend int zwhy::get(hhdy,int);
private:
int height = 160;
int weight = 90;
};
int zwhy::get(hhdy i) {
return i.height;
}
int zwhy::get(hhdy h, int i) {
return h.height * i;
}
int getweight(hhdy i) {
return i.weight;
}
int main() {
zwhy xx;
hhdy hh;
std::cout << xx.get(hh) << std::endl;
std::cout << xx.get(hh,2) << std::endl;
std::cout << getweight(hh) << std::endl;
return 0;
}
类的拷贝赋值与析构
- 拷贝赋值–重载操作符
- 析构:当对象不再存在时销毁的操作。
- 如果不定义相关操作,编译器会默认合成。
- 使用vector和string的类避免分配和释放内存带来的复杂性。
this指针
概念
- 对成员的调用
//定义类成员对象
Sales_data total;
//成员调用
total.isbn();
- 调用成员函数实际是替某个对象调用成员函数,调用函数隐式的指向调用该函数的对象成员。比如上例返回的为total.bookNo里面指向了类total,如何指向这个对象呢?c++里引入了this指针。
- 成员函数通过一个名为this的额外隐式参数来访问它隐式调用的对象。this指针存储的是所使用的类成员对象的首地址。
- this指针不占用类大小,this是一个常量指针,不允许改变this指向的地址。在编写函数中编译器会隐式添加指向类对象的this指针,而在调用函数时函数会隐式添加成员对象的地址。综上,this指针是编译器做的处理,不是自己写的,即他是隐式定义的。
- 考虑到隐式添加的原因,this不能作为参数或变量名。
//代码非法,这里为了说明this隐式定义过程
//函数定义
std::string isbn()const{return bookNo;}
std::string Sales_data::isbn(const Sales_data *const this){ //隐式定义展开
return this->bookNo;
}
//函数调用
Sales_data total;
total.isbn(&total);
常量this指针
- 如果类成员函数不改变this所指的对象,且存在在常量对象上调用普通成员函数的问题。可以将this设置为指向常量的常量指针。方法是在参数列表之后加上const关键字。这种成员函数称为常量成员函数。
std::string isbn()const{return bookNo;}
返回this指针
- 函数也可以返回一个this对象,需要返回原对象或者继续在原对象上做修改时,需要将返回类型定义为“Screen&”类型,加引用符。否则之后的调用只能改变原对象的临时副本,而非原对象。
class Screen {
public:
typedef std::string::size_type pos;
//using pos = std::string::size_type;
Screen& set(char);
Screen& set(pos, pos, char);
Screen& move(pos, pos);
Screen move1(pos a, pos b);
Screen set1(char c);
pos cursor = 0;
pos height = 0, width = 0;
string contents = "stringhh";
};
inline Screen& Screen::move(pos a, pos b) {
pos row = a * width;
cursor = row + b;
return *this;
}
inline Screen Screen::move1(pos a, pos b) {
pos row = a * width;
cursor = row + b;
return *this;
}
inline Screen& Screen::set(char c) {
contents[cursor] = c;
return *this;
}
inline Screen Screen::set1(char c) {
contents[cursor] = c;
return *this;
}
inline Screen& Screen::set(pos a,pos b,char c) {
contents[a*width+b] = c;
return *this;
}
int main() {
Screen scr;
cout << scr.contents << endl;
scr.move(0, 4).set('#'); //带引用,move函数返回的是scr,set修改scr
cout << scr.cursor << endl;
cout << scr.contents << endl;
scr.move1(0, 6).set('$'); //不带引用,move后返回的是scr的副本,set修改的是scr的副本。
cout << scr.cursor << endl;
cout << scr.contents << endl;
scr.set('@');
cout << scr.contents << endl;
return 0;
}
对于常量函数返回常量指针
- 对于不需要修改数据成员的函数,可以将函数定义为const类型。当此时返回this时,由于对象将是指向const的指针,返回的this是const对象的引用。不能把该函数嵌入到动作序列中去。
- 通过区分成员函数是否是const的,可以对成员函数进行重载。
//见display函数
class Screen {
public:
typedef std::string::size_type pos;
//using pos = std::string::size_type;
Screen& set(char);
Screen& set(pos, pos, char);
Screen& move(pos, pos);
Screen move1(pos a, pos b);
Screen set1(char c);
Screen& display(std::ostream& os);
const Screen& display(std::ostream& os) const;
pos cursor = 0;
pos height = 0, width = 0;
std::string contents = "stringhh";
private:
void do_display(std::ostream& os)const {
os << contents<<std::endl;
}
};
inline Screen& Screen::move(pos a, pos b) {
pos row = a * width;
cursor = row + b;
return *this;
}
inline Screen Screen::move1(pos a, pos b) {
pos row = a * width;
cursor = row + b;
return *this;
}
inline Screen& Screen::set(char c) {
contents[cursor] = c;
return *this;
}
inline Screen Screen::set1(char c) {
contents[cursor] = c;
return *this;
}
inline Screen& Screen::set(pos a,pos b,char c) {
contents[a*width+b] = c;
return *this;
}
inline Screen& Screen::display(std::ostream&os) {
do_display(os);
return *this;
}
inline const Screen& Screen::display(std::ostream& os) const{
do_display(os);
return *this;
}
int main() {
Screen scr;
scr.display(std::cout);
scr.move(0, 4).set('#');
std::cout << scr.cursor << std::endl;
scr.display(std::cout);
scr.move1(0, 6).set('$');
std::cout << scr.cursor << std::endl;
scr.display(std::cout);
scr.set('@');
scr.display(std::cout);
return 0;
}
对于整个类
每个类类型唯一
类定义步骤
- 编译成员声明
- 类全部可见后编译函数体,此时函数体可以使用类中定义的任何名字
类名字查找
对于类型名查找
- 对于成员函数声明:先查找类内该声明前是否存在相关声明,没有再去外层作用域上层查找,都没有报错。
- 对于成员函数定义内声明:类内上下寻找相关声明,没有再去外层作用域上层查找,都没有报错。
//情况1 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型,int,height是内层的hhdy height,int类型。
hhdy balance(){return height;}
private:
//using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况2 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是string类型,height是内层的hhdy height,string类型。
string balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况3 报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型int,height是内层的hhdy height,是string类型,报错。
hhdy balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
对于类内对象查找
string hh = "外部";
class zwhy {
public:
string balance(string hh) {
cout << hh << endl; //传参
cout << this->hh << endl; //类内
cout << zwhy::hh << endl;//类内
cout << ::hh << endl; //外部
hh = "函数内";
cout << hh << endl; //函数内
cout << this->hh << endl;//类内
cout << zwhy::hh << endl;//类内
cout << ::hh << endl;//外部
return hh;
}
private:
string hh = "类内";
};
int main() {
zwhy a;
a.balance("传参");
return 0;
}
类使用
1.类对象使用
Sales_data item1;
class Sales_data item2; //C继承来
struct Sales_data item3;
2.类成员函数使用
c++类成员函数作为回调函数
当类成员函数作为函数回调时:类A中有函数a,类B中有函数b,在b中想回调函数a时:
function:函数对象的容器,如function<int(int,int)> fun,fun是一个函数模板,接受(int,int),返回int。
bind:通过bind将函数绑定到一个类成员对象上,包括类成员函数地址&A::a,类成员对象(比如this指针,或者类对象指针&aa),和函数形参(std::placeholders::_1、std::placeholders::_2…等)。
class hhdy {
public:
friend class zwhy;
hhdy() {};
hhdy(int i):num(i){}
int get_num(int i) { return i + num; }
private:
int num;
};
typedef std::function<int(int)> fun;
class zwhy {
public:
zwhy() {};
zwhy(int i) :num(i) {}
int get_num(int i,fun f) { return f(i); }
private:
int num;
};
int main() {
zwhy xx(2);
hhdy hh(1);
fun foo = std::bind(&hhdy::get_num, &hh,std::placeholders::_1);
std::cout<<xx.get_num(3, foo)<<std::endl; //3+1
return 0;
}
类声明
- 前向声明:可以仅声明类,暂时不使用它。此时是一个不完全类型。可以定义(不能使用)指向这种类型的指针或引用,可以在声明(不能定义)中做为参数或返回类型。
- 创建类对象之前必须被定义过,使用指针和引用前也必须定义该成员。
- 类允许包含指向指向自身类型的引用或指针。
类的作用域
- 在类作用域之外的访问:
Screen scr;
Screen *p = scr;
char c = scr.get(); //类对象访问类成员
c = p->get(); //类指针访问类成员
- 对于定义在类外部的成员:对于类外部成员函数定义,必须同时提供类名和函数名;对于函数返回类型,如果返回类型是类内部定义类型(不包括类本身),需要提供返回类型的所在类。
聚合类
聚合类
- 聚合类使用户可以直接访问其成员,并具有特殊初始化语法形式,满足如下条件:
所有成员都是public;
没有定义任何构造函数;
没有类内初始值;
没有基类和virtual函数;
- 可以通过初始化列表进行聚合类初始化,此时初始值顺序必须与声明顺序一致,未初始化部分会被值初始化。
class hhdy {
public:
int hh;
int xx;
std::string zwhy;
};
int main() {
hhdy h1 = {60,90,"hhdyzwhy"};
std::cout << h1.zwhy << std::endl;
return 0;
}
字面值常量类(待补)
- 数据成员都是字面值类型的聚合类是字面值常量类。
数据成员必须是字面值类型;
类必须至少含有一个constexpr构造函数;
含有的类内初始值必须是常量表达式;如果成员属于类类型,必须使用成员自己的constexpr构造函数。
类必须使用析构函数默认定义,该成员负责销毁类的对象。
- constexpr构造函数