类的基本思想是数据抽象data abstraction和封装 encapsulation。
数据抽象是一种依赖于接口 interface和实现 implementation分离的编程和设计技术
封装实现了类的接口和实现的分离
c++ Primer 第五版 第230页
成员函数的声明必须在类的内部,他的定义既可以在类的内部也可以在外部。作为接口组成部分的非成员函数,定义和声明都在类的外部
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 add(const Sales_data&,, const Sales_data&);
std::ostream &print(std::ostream&,, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
我们再观察一次对isbn成员函数的调用
total.isbn()
当我们调用成员函数时,实际上是在替某个对象调用它。如果isbn指向Sales_data的成员(例如bookNo),它隐式的指向调用该函数的对象的成员。在上面的调用中,isbn隐式的返回total.bookNo
成员函数通过this的额外隐式参数来访问调用他的对象,也就是说编译器负责将total的地址传递给isbn的隐式形参this
//伪代码 说明调用成员函数的实际执行过程
Sales_data::isbn(&total);
在成员函数内部 我们可以直接调用该函数的对象成员
,而无需通过成员访问运算符做到这一点,因为this所指的正是这个对象。也就是说isbn使用bookNo时,就像我们书写了this->bookNo一样
isbn的另一个关键之处是紧随参数列表后面的const,他的作用是隐式修改this指针的类型
默认情况下,this的类型是指向类类型非常量版本的常量指针
在成员函数中,this的类型是Sales_data * const,隐式参数仍然要遵循初始化规则,意味着(在默认情况下),我们不能把this绑定在 一个常量对象上(因为底层是一个非const),这种情况使得我们不能在一个常量对象上调用普通的成员函数
如果isbn是一个普通函数且this是一个普通指针参数u,那么可以声明成const Sales_dataconst,但由于this是隐式的,所以如何做出类似的声明就成了一个问题了。
C++的做法是允许把const放在成员函数的参数列表之后,紧随在参数列表之后的const代表this是一个指向常量的指针。这样使用const的成员函数称为常量成员函数*
可以将其想象成
//伪代码 说明其是如何使用的
//非法的,不能显示定义this
std::string Sales_data::isbn(const Sales_data * const this)
{return this->isbn;}
常量对象以及常量对象的引用或指针执泥调用常量对象函数
c++ Primer第五版 第234页
类的作者常常定义一些辅助函数,虽然操作从概念上属于类的接口的组成部分,当并不将其定义为类的成员函数
一般来说,如果非成员函数是类接口的组成部分,这些类的声明应该与类放在一个头文件里面
istream &read(istream &is,Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os<< item.isbn()....;
return os;
}
注意
- 由于IO类不能被拷贝,所以只能通过引用来传递他们,
- print不负责换行,尽量减少对格式的控制,可以确保由用户代码来决定是否换行
c++ Primer 第五版 第235页 构造函数
构造函数的名字和类名相同,和其他函数不同,构造函数没有返回类型
构造函数不能被声明为const的
编译器创造的构造函数被称为合成的默认构造函数
对大多数类来说,它的操作是
- 存在类内初始值的,用于初始化成员
- 否则,默认初始化
只有类没有声明任何构造函数时,编译器才会自动生成默认构造函数
如果类包含有内置类型或者复合类型的成员,只有这些成员都被赋予了类内初始值时,这个类才适合合成的默认构造函数
struct Sales_data{
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(n*p){}
Sales_data(std::istream &);
//其他已有的
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data() = default;在C++11标准中,如果我们需要默认行为,可以用=default来要求编译器生成构造函数
对于第二个和第三个构造函数,在冒号和花括号之间新出现的部分,,称为构造函数初始化列表
当某个成员变量被初始化列表忽略时,他将以与合成默认构造函数相同的方式隐式初始化
c++Primer 第五版 第241页
类可以允许其他类或者函数访问它的非公有成员,方法是将其声明为类的友元(friend)
友元声明只能出现在类的内部,位置不限。不过一般来说,最好在类定义开始或者结束前的位置集中声明友元。
c++Primer 第五版 第242页 封装的益处
封装有两个重要的优点:
- 确保用户代码不会无意间破坏封装对象的状态
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别代码
C++ Primer第五版 第242页 友元的声明
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,因此在友元声明之外需要再专门对函数进行一次声明
由于我们前面提到友元声明通常放在类同一个头文件中,因此我们的Sales_data头文件应该为友元函数在类外部提供声明(除了类内部以外)
c++ Primer 第五版 第245页 类内联函数
定义在类内部的成员函数是自动inline的
我们可以在类的内部将inline作为声明的一部分显式支出,也可以在类的外部用inline修饰其定义
虽然我们无需在声明和定义同时说明inline,但这样做是合法的。一般最好在类外部定义的地方说明inline,这使得类更容易理解
当然inline成员函数应该与相应的类定义在同一个头文件中
c++Primer 第五版第245页 可变数据成员
有时(但并不频繁)会有一种情况,我们希望修改类的某个数据成员,即使在const成员函数中,这时可以通过mutable关键字来实现
class Screen{
private:
mutable size_t access_ctr;
public:
void some_member() const;
};
void Screen::some_member() const{
++access_ctr;
}//const成员函数仍然可以修改mutablemember
c++Primer 第五版 第247页 从const成员函数返回*this
一个const成员函数如果以引用的形式返回*this,那他的返回类型将是常量引用
如果display是一个const成员,返回类型为const Screen&,这样其返回值无法被修改
Screen myScreen;
myScreen.display(cout).set('*');
//如果display返回常量引用,set将引发错误
通过区分成员函数是否是const的,我们可以对其进行重载。由于非常量版本的函数对常量对象不可用,所以我们只能在常量对象上调用const成员对象
在下面这个例子中,定义一个do_display进行实际工作,
class Screen{
public:
Screen &display(std::ostream &os)
{do_display(os);return *this;}
const Screen &display(std::ostream &os) const
{do_display(os); return *this;}
private:
void do_display(std::ostream &os) const{os<<contents;}
};
和我们之前学的一样,一个成员调用另一个成员时,this被隐式的传递
小建议:
为什么要费力定义一个单独的do_display函数呢?
- 一个基本的原因是避免多处使用同一代码
- 随着类的规模发展,display可能更复杂,相应操作只写一处作用较明显
- 开发过程中可能给do_display加调试信息,最终版本被去掉,因此只定义一处更容易
- 额外的调用不会增加任何开销,因为他在类内定义被隐式的声明为内联函数
实践中,设计良好的c++代码常常包含大量类似于do_display的小函数,完成一组其他函数的“实际”工作。
c++Primer第五版第250页 类的声明
类似于函数,类也可以先声明,而暂时不定义
class Screen;//Screen类的声明
对于一个类来说,我们创建他的对象之前该类必须被定义,而不是仅仅被声明,但也有一种例外情况,当一个类的名字出现后,他就被认为被声明过(不是定义),因此类允许包含指向他自身类型的引用或指针:
class Link_screen{
Screen window;
Link_screen *next;
Link_screen *prev;
};
c++Primer 第五版 第250页 友元
类可以把其他类定义为友元,也可以把其他类(之前定义过的)的成员函数定义成友元
如果一个类指定为友元类,那友元类的成员函数可以访问此类包括非公有成员在内的所有成员
class Screen{friend class Window_mgr;};//友元类
class Screen{friend void Window_mgr::clear(ScreenIndex);};//友元 类成员函数
注意友元关系不存在传递性
尽管重载函数名字相同,但仍然是不同的函数,因此一组重载函数的友元声明必须为每一个分别声明
c++ Primer 第五版 第253页 类的作用域
void Window_mgr::clear(ScreenIndex i){}
由于编译器在处理参数列表之前已明确了我们处于Window_mgr作用域中,所以不必专门说明ScreenIndex是Window_mgr类定义的
另一方面,函数返回类型通常在函数名之前。因此当类成员函数定义在类外部时,返回类型中使用的名字都位于类的作用域之外,这时需要指定他属于哪个类
class Window_mgr{
public:
ScreenIndex addScreen(const Screen&);};
//首先处理返回类型
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s){}
c++Primer 第五版 第254页 名字查找和类的作用域
一般来说内层作用域可以重新定义外层作用域中的名字,即使该名字在内层作用域中使用过
然而,在类中,如果成员使用了外层作用域中的某个名字,而这个名字代表一种类型,则类不能在之后重新定义该名字。
c++Primer 第五版第259页 构造函数初始化
构造函数初始化列表只说明用于初始化成员的值,但并不规定他们具体执行顺序
成员的初始化顺序和他们在类定义中出现的顺序一致
最好令构造函数初始值的顺序与成员声明的顺序一致,如果可能的话,尽量避免使用某些成员初始化其他成员
c++Primer第五版 第261页委托构造函数
c++11允许委托构造函数使用所属类的其他构造函数协助其进行构造
class Sales_data{
public:
Sales_data(std::string s, unsigned cnt):bookNo(s),units_sold(cnt){}
Sales_data():Sales_data("",0){}
Sales_data(std::istream &is): Sales_data()
{read(is,*this);}
};
c++ Primer 第五版第263页 隐式类类型转换
如果类的构造函数只接受一个实参,实际上定义了转换为该类型的隐式转换机制,有时我们称之为转换构造函数
而由于编译器只能自动地执行一步类型转换,因此
string null_book = "9999";
item.combine(null_book);//会临时构造Sales_data转换null_book
//下面的代码隐式使用了两种转换,因此报错
item.combine("9999");
//把"9999"转换为string,再将string转为Sales_data
//如果想完成上述代码 可以
item.combine(string("9999"));//正确 显式string,隐式Salesdata
item.combine(Sales_data("9999"));//正确 隐式string,显式Salesdata
而在要求隐式转换的上下文中,我们可以通过explicit关键字进行阻止
class Sales_data{
explicit Sales_dadta(const std::string &s):bookNo(s){}
explicit Sales_data(std::istream&);
};
item.combine(null_book);//报错
item.combine(cin);//报错
关键字explicit支队一个实参的构造函数有效
尽管编译器不会将explicit构造函数用于隐式转换,但我们可以用于显式强制转换
item.combine(Sales_data(null_book));//正确
item.combine(static_cast<Sales_data>(cin));//正确
C++ Primer 第五版 第267页 字面值常量类
某些类可以是字面值常量类,它可能含有constexpr函数成员,这样的成员必须符合constexpr函数的所有要求,它们都是隐式const的
数据成员都是字面值类型的聚合类是一个字面值常量类
符合下列要求的费聚合类也是字面值常量类:
- 数据成员都是字面值
- 至少含有一个constexpr构造函数
- 如果一个数据成员有类内初始值,则内置类型成员的初始值必须是常量表达式;或者成员属于某种类类型,初始值必须使用成员自己的constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
尽管构造函数不能是const的,但字面值常量类的构造函数可以是constexpr的
constexpr构造函数可以声明成=default,或者删除函数的形式。构造函数必须既符合构造函数的要求(不能包含返回语句),又必须符合constexpr的要求(它能拥有的唯一可执行语句就是返回语句),这两点可知其函数体一般来说是空的
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){}
/**/
private:
bool hw,io,other;
};
constexpr构造函数补习初始化所有数据成员,初始值或使用constexpr构造函数或者一挑常量表达式
c++Primer 第五版 第269页 类的静态成员
static关键字使其成为类的静态成员,静态成员可以是public或者private的
类似的静态成员函数不与任何对象绑定在一起,不包含this指针,不能声明为const,且函数体内不能使用this
和类的所有成员一样,当我们只想类外部的静态成员时,必须指明所属的类。static关键字只出现在类内部的声明语句中。
静态数据成员不由类的构造函数初始化。而且一般来说,我们不在类内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次
类似于全局变量,一单定义,静态数据成员就一直存在于程序的整个声明周期中。
double Account::interestRate = initRate();
//定义并初始化一个静态成员
通常情况下类的静态成员不应该在类内初始化,但是我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr
class Account{
public:
static double rate(){return interestRate;}
static void rate(double);
private:
static constexpr int period = 30;//
};
c++Primer 第五版 第271页
静态成员独立于任何对象。因此在某些非静态成员非法的场合,静态成员却可以正常使用。例如,静态数据成员可以是不完全类型(249页),特别地,静态数据成员的可以就是他所属的类类型。而非静态成员受到限制,只能声明它所属的类的指针或引用:
class Bar{
public:
//
private:
static Bar mem1;//正确,静态成员可以 是不完全类型
Bar *mem2;//正确 指针成员可以是不完全类型
Bar mem3;// 错误,数据成员必须是完全类型
};
//另外一个区别是我们可以使用静态成员作为**默认实参**
class Screen{
public:
Screen& clear(char=bkground);
private:
static const char bkground;
};
非静态数据成员不能作为默认实参,因为他的值本身属于对象的一部分,这样做无法真正提供一个对象以便从中获取成员的值,从而引发错误