类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现的分离。隐藏了实现的细节,用户只能使用接口而无法访问实现部分。类要想实现数据的抽象和封装。需要首先定义一个数据抽象类型(abstract data type)。在抽象数据类型中,由类的设计者考虑类的实现过程;实用类的程序员则只需要抽象的考虑类型到底做了什么。无需了解细节。
1、定义抽象数据类型
1.1、定义Sales_data类型:
struct Sales_data {
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
改进的Sales_data类:
struct Sales_data {
string isbn() const { return bookNo; } //const放在函数体与定义之间的位置,
//表达了这个函数体是只读的,不可被修改
//声明为引用是为了阐释return *this是当前的对象本身
Sales_data & combine(const Sales_data &);//
double avg_price() const;//
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data&);
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。用请求该函数的对象的地址来初始化this。
仍何对类成员的访问都被看作this的隐式引用。例如,如果调用
total.isbn()
则编译器负责把total的地址传给isbn的隐式形参this,则可以等价的认为编译器将该调用重写成了如下形式:
Sales_data::isbn(&total)
其中,调用Sales_data的isbn成员时传入了total的地址。
std::string isbn( ) { return bookNo; }
std::string isbn( ) { return this->bookNo; }
//二者是等价的
因为this的目的总是指向“这个”对象,所以this是一个常量指针,我们不允许改变this中保存的地址。
1.3、引入const成员函数
this的类型是Sales_data *const,它需要初始化,但是我们不能把它绑定到一个常量对象上。C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后的const表示this是一个指向常量的指针。像这样使用const的成员函数称为常量成员函数。
//下面代码非法,因为我们不能显式定义this
//this是一个指向常量的指针,因为isbn是一个常量成员
string Sales_data::isbn(const Sales_data *const this) {
return this->isbn;
}
因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容。
类作用域和成员函数
如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性,同时,类外部定义的成员的名字必须包含他所属的类名。
double Sales_data::avg_price() const {
if (units_sold)
return revenue / units_sold;
else
{
return 0;
}
}
函数名Sales_data::avg_price使用作用域运算符来说明如下事实:定义了一个Sales|_data类下的avg_price函数。
1.首先,在名字所在的块中寻找其声明语句,只考虑在名字使用之前出现的声明
2.如果没找到,继续查找外层作用域
3.如果最终没有找到匹配的报错
类的定义分两步处理
1).首先编译成员的声明
2).直到类全部可见后才编译函数体。
定义一个返回this对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
当调用此函数时,
total.combine(trans);
units_sold += rhs.units_sold; //把rhs的成员添加到this对象的成员中
return *this; //返回调用该函数的对象
其中,return语句解引用this指针以获得执行该函数的对象,换句话说,上面这个调用返回的是total的引用。
1.4、定义类相关的非成员函数
属于类的接口组成部分,但不属于类的本身。
I/O类不能被拷贝。
执行输出的函数应该减少对格式的控制,这样可以确保由用户代码来决定是否换行。
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();
return os;
}
1.5、构造函数
构造函数 :构造函数的唯一目的就是为成员赋初值。
定义:类通过一个或几个特殊的成员函数来控制其对象初始化的过程。
只要对象被创建,就会执行构造函数。
默认构造函数:类通过一个特殊构造函数来控制默认初始化过程。默认构造函数无需实参
合成默认构造函数:我们没有定义构造函数时,编译器为我们定义的构造函数。
注意:构造函数没有返回值;构造函数不能被声明称const
struct Sales_data {
//新增的构造函数
Sales_data() = default;//默认构造函数,可以和声明一起出现在类内部,也可以作为定义出现在类的外部
Sales_data(const string &s) :bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(istream &);
string isbn() const { return bookNo; } //const放在函数体与定义之间的位置,
//表达了这个函数体是只读的,不可被修改
//声明为引用是为了阐释return *this是当前的对象本身,不
Sales_data & combine(const Sales_data &);//
double avg_price() const;//
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data(const string &s) :bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
冒号以及冒号和花括号之间的代码,其中花括号定义了(空的)函数体,我们把冒号后的部分称为构造函数初始值列表,他负责为新创建的对象的一个或者几个数据成员赋初值。
在类外部定义构造函数
Sales_data::Sales_data( std::istream &is)
{
read(is, *this); //read函数从is中读取一条信息存到this中去。
}
eg:
class Sales_data
{
public:
Sales_data() = default;
Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0.0) { }
Sales_data(std::string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p) { }
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data &rhs);
double avg_price() const { return revenue/units_sold; }
Sales_data& operator=(const Sales_data &rhs);
private:
std::string bookNo = "w"; //编号
unsigned units_sold = 0; //销售数量
double revenue = 0; //总销售额
//double price; //单价
};
int main()
{
Sales_data item1;
Sales_data item2("wangweihao");
Sales_data item3("wangweihao", 10, 10000);
print(cout, item1) << endl;
print(cout, item2) << endl;
print(cout, item3) << endl;
return 0;
}
情况1
去掉 Sales_data( ) = default;
情况2
当我们不提供类内初始值(c++11)时,默认构造函数不会帮我们初始化内置类型,如int, double等等。
情况3
使用流初始化的构造函数
Sales_data(istream &is)
{
read(is, *this);//read从is中读取一条信息存到this对象中。
}
main()
{
Sales_data item(cin); //就会从流中读取一条信息。
}
1.6、访问控制与封装
使用访问控制符增加类的封装性
public: 成员可以在整个程序内被访问,public定义类的接口
private: 成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节
我们也可以使用struct 和 class 定义类( 它俩的唯一区别就是默认的访问权限 )
struct 在第一个访问说明符之前默认是public的
class 在第一个访问说明符之前默认是private的
1.7、友元
类允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元
如果想把一个函数作为友元,只需要添加一条以friend关键字开头的函数声明即可
struct Sales_data {
//为类的非成员函数做友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend istream &read(istream,Sales_data&);
friend ostream &print(ostream& ,const Sales_data&);
//新增的构造函数
Sales_data() = default;//默认构造函数,可以和声明一起出现在类内部,也可以作为定义出现在类的外部
Sales_data(const string &s) :bookNo(s) {}
Sales_data(const string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n) {}
Sales_data(istream &);
string isbn() const { return bookNo; } //const放在函数体与定义之间的位置,
//表达了这个函数体是只读的,不可被修改
//声明为引用是为了阐释return *this是当前的对象本身,不
Sales_data & combine(const Sales_data &);//
double avg_price() const;//
private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&,const Sales_data&);
ostream &print(ostream&, const Sales_data&);
istream &read(istream&,Sales_data&);
注意:
friend std::istream& read(std::istream &is, Sales_data &rhs);
//注意如果read函数在其他.h文件中定义,我们必须在用它的类的.h文件中先声明它在设置为友元
一般来说,最好在类定义开始或结束前的位置集中声明友元
类还可以把其他的类定义成友元,也可以把其他类(之前定义过的)的成员函数定义为友元。
此外,友元函数能定义在类的内部,这样的函数是隐式内联的
如果类指定的友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
令成员函数作为友元
重载函数作为友元,尽管名字相同,但是他们依然是不同的函数。要分别对每一个函数进行声明
class Screen
{
public:
typedef string::size_type pos;//与下面的方式等价
//using pos = string::size_type;//使用类型别名等价的声明一个类型名字
Screen() = default;//因为Screen有另一个构造函数,所以本函数是必须的
//cursor被其类内初始值初始化为0
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); //能在之后被设为内联
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
同样,我们可以在类外部使用inline关键字修饰函数的定义(最好的做法)
inline //可以在函数的定义处指定inline
Screen &Screen::move(pos r, pos c) {
pos row = r* width;
cursor = row + c;
return *this;
}
//const代表函数get不可以修改
char Screen::get(pos r, pos c)const
{
pos row = r * width;
return contents[row + c];
}
可变数据成员
用mutable修饰的成员永远不会是const ,即使出现在const函数中
class Screen
{
public:
typedef string::size_type pos;//与下面的方式等价
//using pos = string::size_type;//使用类型别名等价的声明一个类型名字
Screen() = default;//因为Screen有另一个构造函数,所以本函数是必须的
//cursor被其类内初始值初始化为0
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;
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
mutable size_t access_ctr;
};
void Screen::some_member() const{
++access_ctr;//保存一个计数值,用于记录成员函数被调用的次数
}
我们希望类开始时总是有默认初始值, 在c++11中,最好的方式就是把这个默认值声明成一个类内初始值。
类内初始值必须以=号或者花括号表示。
class Widow_mgr {
private:
//这个window_mgr追踪的Screen默认情况下,一个window_mgr包含一个标准尺寸的空白Screen
vector screens{ Screen(24,80,' ') };
};
myScreen.mov(4,0).set('c');
//等价于上面这个
myScreen.move(4,0);
myScreen.set('#');
这句话的含义是mov移动到(4,0)位置设置此出的字符为'c'。
如果函数mov返回的是非引用类型,代码将不会正确执行,因为此时返回的是*this副本返回
以副本返回输出有可能也是副本,不要错误的认为真实的就改变了。
Screen& mov(pos h, pos w)
{
cursor = h*width + w;
return *this;
}
一个const 成员函数以引用的形式返回的*this,那么它的返回类型也是常量引用。
this指针的好处
在类中区分类的变量和类方法的变量
#include
class A
{
public:
void fun()
{
int a = 5;
std::cout << a << std::endl; //区分局部变量和成员变量,不过我们尽量不要这样做,可以重新起个名字
std::cout << this->a << std::endl;
}
private:
int a = 10;
};
int main()
{
A n;
n.fun(); //输出结果是5和10
return 0;
}
一个类的所有实例调用的方法在内存中只有一份拷贝,尽管可能存在多个对象,
每个对象在内存中都有一份拷贝,this允许方法为不同的对象工作,每次调用
方法时, this变量会变成引用该方法的特定的实例,方法代码接着会和特定的对象相关联。
初始值有时必不可少:
#include
class fun
{
public:
void func(){ std::cout << "func" << std::endl; }
fun(): k(1) { }
private:
const int i; //提供默认函数初始化,但是一旦定义了型如上面的构造函数就会报错,因为不会默认合成构造函数,引用和const就没有被初始化了
int &j;
int k;
};
int main()
{
int i = 10;
int &r = i; //引用和const必须要初始化。
//int &k;
//const int n;
}
成员初始化顺序
最好的是用构造函数的参数作为成员的初始值,避免成员初始化顺序带来的问题。
X(int val):i(val),j(val){}
Sales_data(std::string s = " "):bookNo(s) { std::cout << "Sales_data(std::string s = " ")" << std::endl; }
Sales_data(std::string s, unsigned cnt, double rev) : bookNo(s), units_sold(cnt), revenue(rev)
{ std::cout << "(std::string s, unsigned cnt, double rev) : " << std::endl; }
Sales_data(std::istream &is = std::cin) { read(is, *this); std::cout << "std::istream &i = cin" << std::endl;}
c++11 委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数
加入委托的构造函数函数体里面包含有代码的话,先执行这些代码,然后控制权才会交还给委托者的函数体
列子
#include
#include
class Sales_data
{
public:
Sales_data(std::string bn, unsigned us, double re): bookNo(bn), units_sold(us), revenue(re)
{ std::cout << "委托Sales_data(std::string bn, unsigned us, double us) \n执行函数体" << "\n"<< std::endl;}
Sales_data(): Sales_data("", 0, 0) //委托三参版本
{ std::cout << "归还Sales_data():Sales_data("", 0, 0) \n执行函数体" << "\n" << std::endl; }
Sales_data(std::string s): Sales_data(s, 0, 0) //委托三参版本
{ std::cout << "归还Sales_data(std::string):Sales_data(s, 0, 0) \n执行函数体" << "\n" << std::endl;}
Sales_data(std::istream &is) : Sales_data() //委托默认版本,默认版本在委托三参版本。
{
read(is, *this);
std::cout << "执行Sales_data(std::istream &is) \n执行函数体" << "\n" << std::endl;
}
friend std::istream& read(std::istream& is, Sales_data& item);
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
std::istream& read(std::istream& is, Sales_data& item)
{
is >> item.bookNo >> item.units_sold >> item.revenue;
return is;
}
int main()
{
std::cout << "a" << std::endl;
Sales_data a;
std::cout << "b" << std::endl;
Sales_data b("string");
std::cout << "c" << std::endl;
Sales_data c(std::cin);
}
C++没有提供让一个构造函数去委托另一个构造函数执行构造操作的机制。这意味着不能(或不提倡)
使用缺省参数,类的维护者不得不编写并维护多个构造函数。这会导致源代码和目标代码的重复,降低
了可维护性(由于可能引起不一致性),有时还会导致代码膨胀
隐式的类类型转换
我们也能为类定义隐式的转换机制,如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,
有时把这种构造函数称为转换构造函数。
class Sales_data
{
public:
Sales_data() = default;
Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0) { }
Sales_data(std::istream &is):{ read(is, *this);}
};
Sales_data s1;
string s = "hello";
s1.combine(s);
成立,因为存在只传参string s的构造函数。
那么s1.combine(s);中的s会调用Sales_data的只传参数s的版本的构造函数,那么s1.combine(s)的s就会变成一个Sales_data对象,所以可以调用combine函数。
我们可以抑制构造函数定义的隐式转换,既不能通过穿参数s隐式构造一个Sales_data对象。
通过将构造函数声明为 explicit 加以阻止
class Sales_data
{
public:
Sales_data() = default;
explicit Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0) { }
explicit Sales_data(std::istream &is):{ read(is, *this);}
};
static
<1.静态成员函数可以访问静态成员变量和和静态成员函数
<2.非静态成员函数也可以访问静态成员变量和和静态成员函数
<3.静态成员函数没有this指针,无法访问属于类对象的非静态成员变量和非静态成员函数
<4.由于没有this指针的额外开销,因此静态成员函数与类的非静态成员函数相比速度上会有少许的增长
<5.静态成员函数/变量属于整个类,没有this指针,该类的所有对象共享这些静态成员函数/变量
<6.非静态成员函数/变量属于类的具体的对象,this是缺省的
<7.静态成员变量在类内声明,且必须带static关键字;在类外初始化,且不能带static关键字
<8.静态成员函数在类内声明,且必须带static关键字;在类外实现,且不能带static关键字