C++学习笔记:类的基础知识
1.定义抽象数据类型
类的基本思想:数据抽象+封装
数据抽象依赖于接口和实现分离的编程技术。
数
据
抽
象
{
接
口
{
用
户
能
执
行
的
操
作
实
现
{
类
的
数
据
成
员
负
责
接
口
实
现
的
函
数
体
定
义
类
需
要
的
各
种
私
有
函
数
数据抽象\begin{cases} 接口\begin{cases}用户能执行的操作\end{cases}\\ 实现\begin{cases}类的数据成员\\负责接口实现的函数体\\定义类需要的各种私有函数\end{cases}\\ \end{cases}
数据抽象⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧接口{用户能执行的操作实现⎩⎪⎨⎪⎧类的数据成员负责接口实现的函数体定义类需要的各种私有函数
封装实现了类的接口和实现的分类。封装后的类隐藏了实现细节。
类要实现抽象和封装,首先定义一个抽象数据类型。
1.1定义成员函数
成员函数必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。
class Sales_data{
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
};
对于上述类,isbn函数定义在了类内,combine和avg_price定义在了类外。
Sales_data total;
total.isbn();
使用点运算符访问total的isbn成员,然后调用。
成员函数通过this的额外隐式参数方位调用它的对象。调用一个成员函数时,用该对象的地址初始化this。
total.isbn();
实际上吧total的地址传给了isbn的隐式参数this。可以等价认为编译器将函数重写为如下的形式:
Sales_data::isbn(&total)
,传入了total的地址。
在成员函数内部,任何对成员的访问都被看做对this的隐式调用。当isbn使用bookNo时,就像我们书写了this->bookNo
一样。
this是一个常量指针,其中保存的地址不能改变。(常量指针,指向一个常量的指针,也就是这个地址是不能变的)
1.2 引入const成员函数
std::string isbn() const {return bookNo};
isbn后有一个const关键字,这里的const作用是修改隐式this指针的类型。
std::string isbn() const {return bookNo};
像这样使用const的成员函数被称为常量成员函数。常量成员函数不能改变调用它的对象的内容——只读成员函数。
1.3 类的作用域和成员函数
类的成员函数体可以任意使用类中的其他成员,而不用在意其出现的顺序:就算属性写在成员函数后面,成员函数也可以对其进行访问。
在类的外部定义成员函数要使用作用域运算符::
double Sales_data::avg_price() const{
}
注意先写返回值类型,然后是类名,作用域运算符,最后是函数名和形式参数列表。
假如是构造函数在类外部实现,那么就不写一开始的返回值。
Sales_data::Sales_data(){
}
1.4 定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
};
上面的参数传递方式是pass by reference的方式,假如我们使用调用语句:total.combine(trans)
,那么trans将会和rhs绑定,rhs是一个trans的引用,而不会将trans的内容以字节复制到rhs中。也就是说,若是在函数中trans会被修改,也必须使用这种pass by reference的方式(或者传指针),这样trans的内容才会在函数体中被修改。如果是pass by value,即在上述函数的参数列表中为rhs而非&rhs,那么trans的内容以字节复制到rhs中。离开了combine后对trans内容进行的修改将不会反映在trans上,你只是修改了局部变量rhs的内容。
值得注意的是这个函数的返回类型和返回语句。return 语句解引用this指针获得对象,也就是说这个对象返回的是total的引用。
1.5 定义类相关的非成员函数
定义某些函数,这些操作从概念上来说属于类的接口的组成部分,但他本身并不属于类本身。
和成员函数一样,通常把函数的声明和定义分开。如果函数在概念上属于类但是不定义在类中,那他一般应与类声明(而非定义)在同一个头文件内。这种方式下,用户使用接口的任何部分都只需要引入一个头文件。
比如定义一个有关某个类的打印函数:
声明时直接在.h中的class外部进行声明,实现时在对应的.cpp中实现,但是不用写作用域运算符。
声明:
class Sales_data{
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
};
istream &read(istream &is, Sales_data &item);
实现:(无作用域运算符)
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;
}
1.6构造函数
-
特点
-
名字和类名相同
-
无返回值
-
可以进行函数重载
-
构造函数不能被声明成const
当构造函数完成一个对象的初始化过程时,对象才真正取得“常量”属性。因此,const对象是可以用构造函数初始化的。
-
-
合成的默认构造函数
创建对象过程:
Sales_data total; Sales_data trans;
total和trans对象通过默认构造函数(default constructor)进行初始化。默认构造函数没有任何的实参。
注意:当我们自己没有编写构造函数时,编译器为我们隐式定义一个默认构造函数。如果我们定义了,这个默认构造函数将不会被隐式定义。
默认构造函数的初始化规则:
- 如果存在类内初始值,用它来初始化成员。
- 否则默认初始化该成员—由类型决定
-
不应该依赖默认构造函数
(1)只有当类没有声明任何的构造函数时,编译器才会自动地生成默认构造函数
(2)合成的默认构造函数可能会执行错误操作
对象含有复合类型或者指针时会得到未定义的值
(3)当某个类中含有其他类型成员,但这个类型未提供默认构造函数时,必须自定义构造函数。
-
实例
class Sales_data{
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//新增的构造函数
Sales_data() = default;
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
Sales_data(std::istream &);
};
=default的含义
默认构造函数,是c++11的新标准。
=default既可以和声明一起出现在类的内部,是内联的;也可以作为定义出现在类的外部,默认不是内联的。
初始化列表
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
初始化列表即是冒号之后,花括号之前的代码。
在类的外部定义构造函数
以Sales_data(std::istream &);
为例:
Sales_data::Sales_data(std::istream &is){//声明时没有写形参,定义时要补上
read(is, this*);
}
没有出现在初始化列表中的成员将通过类内初始值初始化或者执行默认初始化。即bookNo被初始化为空的string对象,units_sold和revenue将是0;
1.7 拷贝,赋值和析构
1、对象的拷贝:
- 初始化变量
- 以值的方式传递或者返回一个对象
2、对象的赋值:
- 使用赋值运算符
3、销毁一个对象
- 当一个vector或者数组被销毁时,存在其中的对象也会被销毁。
编译器的默认合成
在我们不定义时,编译器会替我们合成:
total = trans;
与下面的代码相同:
//Sales_data的默认赋值操作等价于
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
某些类不能依赖合成的版本
含有指针的类对拷贝、赋值、销毁的操作必须得到重写。
2.访问控制与封装
public:整个程序可以访问
private:类的成员函数访问。封装了实现类的细节。
class Sales_data{
private: //增加访问说明符
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
public: //增加访问说明符
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//新增的构造函数
Sales_data() = default;
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
Sales_data(std::istream &);
};
类的访问说明符出现多少次没有限定,先后顺序也没有限定,有效范围是直到出现下一个访问说明符或者类的结尾为止。
使用struct和class定义类的唯一区别是默认的访问权限:
- class的默认访问权限是private
- struct的默认反问权限是public
2.1友元
增加了访问控制权限后我们定义的read或者print函数将无法编译:外部不能访问类的内部的私有成员信息。
为了允许这种操作,增加了友元的概念。
如果要把一个函数作为一个类的友元,那么在函数的最开头加上一个friend关键字再声明即可:
class Sales_data{
//为Sales_data非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
private: //增加访问说明符
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
public: //增加访问说明符
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//新增的构造函数
Sales_data() = default;
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
Sales_data(std::istream &);
};
友元只能出现在类定义的内部。一般来说,集中在类的开始或者结束前的位置集中声明友元。
友元的声明
假如希望类的用户能够调用某个友元,就必须在友元声明之外再专门对函数进行一次声明。
友元的声明与类的本身放置在一个头文件中(类的外部)。即要为read,print,add在Sales_data的头文件中提供独立声明。
3.类的其他特性
3.1 类成员
定义类成员
类可以自定义某种类型在类中的别名。
- 使用typedef
class Screen{
public:
typedef std::string::size_type pos;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
- 使用using语句
class Screen{
public:
using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
成员函数
在类中有些规模较小的函数适合被声明为内联函数。在.h文件的class内部进行定义的函数默认是inline函数。
在类的外部使用inline关键字修饰函数的定义:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xztE2kZV-1621912279296)(C:/Users/%E5%AD%99%E8%95%B4%E7%90%A6/AppData/Roaming/Typora/typora-user-images/image-20210524201253900.png)]
inline函数的声明应该于相应的类的定义在同一个头文件中。
重载成员函数
成员函数也可以被重载。
满足函数名相同,参数的数量和/类型有所区别就行。
可变数据成员
一个被加上mutable关键字修饰的成员被称为可变成员,这样的成员即是是在const修饰的函数——只读成员函数中也可被修改。
class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr; //即使在一个const对象内也能被修改
};
void Screen::some_member() const
{
++access_ctr; //保存一个计数值,用于记录成员函数被调用的次数
}
类数据成员的初始值
3.2 返回this*的成员函数
class Screen{
public:
Screen &set(char);
Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char c)
{
contents[cursor] = c; //设置新值
return *this; //将this对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents[r*width + col] = ch; //设置新值
return *this; //将this对象作为左值返回
}
set成员返回的是调用set对象的引用。返回引用的函数是左值的,意味着,返回的是对象的本身而非对象的副本。把一系列语句连接在一个表达式中:
myScreen.move(4,0).set('#');
之所以可以这样链接也是因为move函数返回的是myScreen对象本身,而非副本。我们接着对这个myScreen进行set操作,改变myScreen对象的值。即前后操作都在一个对象上执行。
这样 语句等价为:
myScreen.move(4,0);
myScreen.set('#')
如果我们令move和set返回Screen而非Screen&,上述语句则会大不相同。那么在这个例子中将会等价为:
Screen temp = myScreen.move(4,0); //对返回值进行拷贝操作
myScreen.set('#') //不会改变myScreen的contents
假如我们定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变副本的值,不能改变原对象的值。
从const成员返回*this
对Screen类添加一个display操作,为了使之能和move,set出现在同一序列中,因此display也应该返回Screen&,即引用。
显示Screen不需要改变内容,所以声明为const。
但是:一个const成员函数如果以引用形式返回*this,那么返回值类型将是一个常量引用。
也就是说,我们mySreen.display(cout).set('*')
将会出现错误。
基于const的重载
在普通的函数中,顶层的const不影响传入函数对象。有无const的形参是无法被区分开来的。也就是说:
Record lookup(Phone);
Record lookup(const Phone); //重复定义
Record lookup(Phone*);
Record lookup(Phone* const); //重复定义 指针常量,指针不能再指向别的对象,但是该对象的内容可以被修改
其实时重复声明了Record lookup(Phone);
。
底层的const,如果形参是某种类型的指针或者引用,通过区分其指向的是常量对象还是非常量对象可以实现函数重载。
Record lookup(Phone&);
Record lookup(const Phone&); //新函数
Record lookup(Phone*);
Record lookup(const Phone*); //新函数 常量指针,指针可以指向别的对象,但是对象的内容不可改
3.3类类型
每个类定义了唯一的类型。即使两个类的成员完全一样,也不是一个类,还是两个类。
struct First{
int memi;
int getMem();
}
struct Second{
int memi;
int getMem();
}
First obj1;
Second obj2 = obj1; //错误,obj1和obj2不是一个类型
类的声明
可以仅仅声明一个类,而不定义,称为向前声明:
class Screen; //向前声明
只是指明了Screen是一个类型。
先完成一个类的定义之后数据成员才能被声明成这种类型。然而一旦一个类的名字出现过,他就被认为是声明过了(但未定义),因此允许包含指向它自身类型的应用或指针。
class Link_screen{
Screen window;
Link_screen *next;
Link_screen *prev;
}
练习:定义一对类X和Y,其中X包含一个指向Y的指针,而Y包含一个类型为X的对象。
class Y;
class X {
Y* y;
};
class Y {
X x;
}
3.3.1 友元再探
类可以定义友元函数,也可以定义友元类。还可以把其他已经定义过的类的成员函数定义成友元。
此外,友元函数能定义在类的内部,这样的函数时隐式内联的。
类之间的友元关系
Screen把Window_mgr指定为他的友元:
class Screen{
//Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mgr;
}
友元类的成员函数可以访问此类包括private成员在内的所有成员。我们想通过Window_mgr的clear成员访问Screen类,可以这样写:
class Window_mgr {
public:
//窗口中每个屏幕的编号
using ScreenIndex = std: :vector<Screen>: :size_ type;
//按照编号将指定的Screen重置为空白
void clear (ScreenIndex) ;
private :
std::vector<Screen> screens {Screen(24, 80,'')};
};
void Window_mgr::clear (ScreenIndex i)
{
// s是一个Sereen的引用,指向我们想清空的那个屏幕
Screen &s = screens
[i] ;
//将那个选定的Screen重置为空白
s. contents = string(s.height * s.width, '');
}
如果不是友元将无法编译。但是注意,友元关系是无法传递的。
每个类负责控制自己的友元。
成员函数作为友元
上述的操作将整个Window_mgr类作为了友元,也可以只给与clear函数以友元函数的权限:
class Screen{
// Windows_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex);
//Screen 类的剩余部分
};
这样的设计必须按照一定的顺序,比如在这个案例中,就要按照一定的顺序:
- 首先定义Window_mgr类,声明clear但不定义。因为在clear使用Screen中的成员之前必须声明Screen
- 定义Screen,包括对于clear的友元声明。
- 最后定义clear,此时才可以访问Screen的成员
函数重载和友元
函数重载了就是不同的函数,所以如果一个类想把一组重载函数作为友元函数,必须对每一个分别进行声明。
友元声明和作用域
主要是理解2.1节中“假如希望类的用户能够调用某个友元,就必须在友元声明之外再专门对函数进行一次声明。”一句话。
在类中也是一样的,如果之前没在类的外部对友元函数进行声明,那么在类的内部是不能使用该友元函数的。
在类的内部声明友元的作用是影响访问权限,它本身并非普通意义上的声明。
struct X {
friend void f() { /*友元函数可以定义在类的内部*/ }
X(){f();} //错误:f还没有被声明.
void g() ;
void h() ;
};
void X: :g(){ return f () ; } //错误:f还没有被声明
void f() ; // 声明那个定义在 X中的函数
void X: :h() { return f() ; } //正确:现在f的声明在作用域中了
4 类的作用域
一个类就是一个作用域。在类的外部定义成员函数必须使用作用域运算符。
class Window_mgr{
public:
//向窗口添加一个Screen, 返回它的编号
ScreenIndex addScreen (const Screen&) ;
//其他成员与之前的版本一致
};
//首先处理返回类型,之后我们才进入Window_mgr的作用域
Window_mgr::ScreenIndex
Window_mgr:: addScreen (const Screen &s){
screens.push back(s) ;
return screens.size ();
}
首先就要声明ScreenIndex是哪个作用域的,之后再进入这个作用域。
4.1 名字查找与类的作用域
名字查找:寻找与所用名字最匹配的声明的过程。
编译器处理完类中的所有声明后才会处理成员函数的定义。
用于类成员声明的名字查找
- 在名字出现之前的块内中寻找作用语句
- 块内找不到扩展到块外寻找
- 最终找不到就报错
类型名要特殊处理
类内作用域可以重新定义外层作用域中的名字。
但若类内使用了外层作用域中的名字就不能再次定义。
成员定义中的普通块作用域的名字查找
比如成员函数的名字查找:
- 先在成员函数内查找该名字的声明。
- 成员函数没找到,类内寻找。类内的所有成员都可以被考虑。
- 类内没找到,成员函数定义之前的作用域内进行查找。
5.构造函数
5.1 初始值列表
其实构造函数如果没有初始值列表,只有大括号中的方法体,在执行这个构造函数时,在执行到方法体之前是会进行默认初始化的。后面进入方法体之后再进行赋值操作。合法但比较草率。
初始值列表必不可少的情形
-
const和引用类型,必须使用初始化列表对其初始化。
-
成员属于某种类型且改种类型没有定义默认构造函数时,也必须将这个成员初始化。
比如:
class ConstRef{ public: ConstRef(int ii); private: int i; const int ci; int &ri; }
上面的ri和ci必须通过初始值列表进行初始化:
ConstRef::ConstRef(int ii):i(ii), ci(ii), ri(i){}
成员初始化顺序
构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序与他们在类定义中出现的顺序一致。
class X {
int i;
int j;
public:
//未定义的: i在于之前被初始化
X(int val) : j(va1), i(j) { }
};
看似使用val初始化j,然后使用j初始化i,其实是使用了未被初始化的j去初始化i!!!!!有的编译器会出现警告。
良好的写法如下:
X(int val): i(val), j(val)
默认实参和构造函数
class Sales_data{
public:
//定义默认构造函数,使其与只接受一个string实参的构造函数功能相同
Sales_data(std::string s = " "): bookNo(s){}
//其他构造函数与之前一致
Sales_data() = default;
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
Sales_data(std::istream &);
}
5.2委托构造函数
委托构造函数:允许使用它所属类的其它构造函数执行他自己的初始化过程。或者说他把自己的一些或者全部功能委托给了其他构造函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWhTJIni-1621912279298)(C:/Users/%E5%AD%99%E8%95%B4%E7%90%A6/AppData/Roaming/Typora/typora-user-images/image-20210524231210895.png)]
除了第一个构造函数外,其他的构造函数都使用了委托构造函数委托了他们的工作。
5.3 默认构造函数的作用
实际中,如果定义了其它构造函数,那么最好提供一个默认构造函数。
使用默认构造函数
Sales_data obj(); //正确,定义了一个函数而非对象
if (obj.isbn() == Primer_5th.isbn()) //错误,obj是一个函数
Sales_data obj2; //正确,定义了一个对象
5.4 隐式的类型转换
转换构造函数:只接受一个实参,是把这个实参类型转换为此类的类型的隐式转换。
class Sales_data{
//为Sales_data非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
private: //增加访问说明符
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
public: //增加访问说明符
//成员函数
std::string isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double avg_price() const;
//新增的构造函数
Sales_data() = default;
Sales_data (const std::string &s): bookNo(s){};
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
Sales_data(std::istream &);
};
在此类中,Sales_data (const std::string &s): bookNo(s){};
Sales_data(std::istream &);`两个构造函数就能把string类型和istream类型转换为Sales_data类型。下面的代码说明了这样的隐式转化的过程:
string null_book = "99999-9999";
//构造一个临时的Sales_data对象
//该对象的units_sole和revenue等于0,bookNo等于null_book;
item.combine(null_book);
只允许一步类类型转换
编译器只会执行一步的类型转换。下面的代码隐式使用了两种规则,所以是错误的:
//函数原型:Sales_data& combine(const Sales_data&);
//经过了两次转换,编译器只能经过一次,故错误
item.combine("999999");
上述程序需要经过的char[ ]—>string—>Sales_data&,这样的转换,但是编译器只能够转换一次,所以错误。
下面的程序则是正确的:
#include<stdio.h>
class X{
int a;
public:
X(){}
X(int);
};
X::X(int a):a(a){
}
int main(){
double d = 3.0;
X x = d; //只经过了double --> int一次转换
return 0;
}
类类型转换不是总有效
定义一个istream到Sales_data的转换:
item.combine(cin);
隐式地把cin转换为Sales_data。这个转换执行接受一个istream到Sales_data的构造函数。这个构造函数创建的对象是一个临时变量。combine结束后我们再也无法访问。
抑制构造函数定义的隐式转换
将构造函数声明为explicit,可以组织上述的隐式转换的发生。
注意只在声明处加explicit,定义处不写explicit,即explicit只允许出现在类内的构造函数声明处。
class Sales_data{
private: //增加访问说明符
//数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
public: //增加访问说明符
//新增的构造函数
Sales_data() = default;
Sales_data (const std::string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {};
explicit Sales_data(std::istream &);
explicit Sales_data (const std::string &s): bookNo(s){};
}
如果这样声明的话,下面的两行代码将都无法通过编译:
item.combine(null_book);
item.combine(cin);
explicit修饰的构造函数只能用于直接初始化
隐式转换也可能发生在执行拷贝形式的初始化时(使用=)。此时我们只能使用直接初始化,不能使用explicit构造函数。
#include<stdio.h>
class X{
int a;
public:
X(){};
explicit X(int);
};
X::X(int a):a(a){
}
int main(){
double d = 3.0;
X x = d; //不能将explicit构造函数用于拷贝形式(=)的初始化
X x2 = X(d); //直接初始化
X x3(d); //直接初始化
return 0;
}
总结:当我们用explicit 关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。
为转换显示地使用构造函数
加上explicit之后编译器不会执行隐式转换,但是可以使用这样的构造函数显示强制进行转换:
item.combine(Sales_data(null_book)); //正确,实参是一个显示构造地Sales_data对象
item.combine(static_cast<Sales_data>(cin)); //正确,static_cast可以使用explicit的构造函数
6.类的静态成员
成员与类本身直接相关,而不是与某个对象保持关联。
这样的成员声明为类的static成员。
6.1 类静态成员的声明和使用
声明:成员声明之前加上static关键字。
静态成员可以是public , private , 的。类型也可以是常量,指针,类类型等。
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 () ;
};
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。Account类可以有多个实例,但是只存在一个interestRate,这个静态变量被所有的实例共享。
静态成员函数不和任何对象绑定:
- 静态成员函数不能被声明为const
- 不含this指针
使用:作用域运算符直接访问
double r;
r = Account::rate(); //作用域运算符直接访问
此外,虽然静态变量不属于某个对象,但还是能使用实例,实例的引用或者指针来访问静态成员。——点和->运算符即可
同时成员函数不通过作用域运算符就能直接访问静态成员。
6.2 静态成员的定义
**对于成员函数:**可以在类的内部或者外部定义成员函数。
在类内部定义时,加上static后正常定义即可。
当在类的外部定义时,不加static关键字,static只出现在类内声明部分。
对于类的成员:
一般来说,不在类的内部初始化静态成员。必须在类的外部定义和初始化每个静态成员。
特殊情况是,我们可以为静态成员提供const整数类型的类内初始值。
static double interestRate;
static double initRate () ;
};
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。Account类可以有多个实例,但是只存在一个interestRate,这个静态变量被所有的实例共享。
静态成员函数不和任何对象绑定:
- 静态成员函数不能被声明为const
- 不含this指针
**使用**:作用域运算符直接访问
```c++
double r;
r = Account::rate(); //作用域运算符直接访问
此外,虽然静态变量不属于某个对象,但还是能使用实例,实例的引用或者指针来访问静态成员。——点和->运算符即可
同时成员函数不通过作用域运算符就能直接访问静态成员。
6.2 静态成员的定义
**对于成员函数:**可以在类的内部或者外部定义成员函数。
在类内部定义时,加上static后正常定义即可。
当在类的外部定义时,不加static关键字,static只出现在类内声明部分。
对于类的成员:
一般来说,不在类的内部初始化静态成员。必须在类的外部定义和初始化每个静态成员。
特殊情况是,我们可以为静态成员提供const整数类型的类内初始值。