文章目录
1 概念
1.1 内部类型与自定义类型
可以将C++类型分为两种,一种是内部类型与自定义类型(class type)。
内部类型:被定义为语言核心的一部分,如char、int和double等。
自定义类型:将相关的数据值组合在一个数据结构中,如string、vector、istream。
1.2 类的设计思想
除了输入-输出库中的某些低级、系统专用的例程之外,库中的类所依赖的语言工具与任何程序能够定义的专用的类型的语言工具是相同的。
很多C++设计依赖这样一个设计思想,我们应该让程序员创建与内部类型一样易于使用的类型。创建具有简明直观的接口的类型,除了有实质的语言支持外,还要求我们具有在类的设计过程中的体验和判读。
2 设计类
2.1 自定义类型
为了不让用户直接访问数据,将对象的存储方式的实现细节隐藏起来,我们要求用户仅通过函数访问对象。
因此我们需要向用户提供方便对对象的操作,这些操作构成了我们的类接口(函数)。
在头文件中自定义类时,我们应该使用限定名(如std::string等)。为的是供他人使用的代码,应该包含最少数量的必要声明。
2.2 成员函数
通用规则:如果一个函数会改变一个对象的状态,那应该作为这个对象的成员。
成员函数:类对象的一个成员。
- 1 为了调用成员函数,用户必须指明被调用的函数是对象的一个成员。(使用::【作用域运算符】)
- 2 在对象的成员函数中,不需要新建对象作为参数进行传递。
- 在调用向量或字符串时对象的一个成员函数时,必须指明自己想用哪一个向量或字符串。(如string s;中可以调用s.size(),但是如果不指明一个字符串对象,就无法调用string类的size()函数,也就是要实例化对象)
- 3 在成员函数中,可以直接访问对象的数据元素。
- 在成员函数内部引用成员,无需使用限定形式,因为引用那个正是操作的对象的成员。
示例:
- 在成员函数内部引用成员,无需使用限定形式,因为引用那个正是操作的对象的成员。
//类
struct info{
std::string name;
//成员函数
std::istream& read(std::istream&);
}
//调用成员函数
istream& info::read(istream& in){
in >> name;
return in;
}
对比版本:
//类
struct info{
std::string name;
}
istream& info::read(istream& in, info& s){
in >> s.name;
return in;
}
如果将::放在一个名称之前,表明我们要使用这个名称的某一个版本,而所使用的这个版本不能称为任何事物的成员。
如下面示例所示,如果我们希望调用非成员函数add(带有一个参数),我们就要使用::,否则,编译程序会认为我们所指示的是info::add,从而提示错误——因为在调用中使用了过的参数。(这个非成员函数要在头文件中声明)
2.2.1常量成员函数
在下面程序中将add成员函数声明为const(参数列表后加了const),表明函数里不能修改对象的值。
因此我们可以用常量对象调用它。但是我们不能对常量对象调用非常量函数。例如常量info对象不能调用read函数,因为它能修改对象的状态。
在调用成员函数时,如果函数是某个对象的成员,那么这个对象就不能是一个参数。因此,无法在参数列表中指明这个对象是const。
但是我们可以对函数本身进行限制,这样就能让它称为一个常量(成员函数)。在类内部定义的函数声明和函数定义中都要包括限定此const。
注意:即使一个程序从未直接创建过任何常量对象,仍然有可能在函数调用的过程中创建许多对常量对象的引用。如果将一个非常量对象传递给一个具有常量引用参数的函数,这个函数就会将这个对象当常量进行处理,编译器将只允许它调用这个对象的常量成员。
总结来说,就是非常量非成员函数可以访问常量和非常量类成员,但是常量非成员函数只可以访问常量类成员函数。
示例程序
头文件:
#ifndef TEST_info
#define TEST_info
double add(double , double);//必要的声明,为了在源文件中使用::
double add(double);
double deal(double, double);
#endif
#include <iostream>
using std::cin; using std::cout;
using std::endl;
#include <string>
using std::string;
#include "test.h"
struct info{
std::string name;
double mid, final;
double add() const;
std::istream& read(std::istream&);
};
istream& info::read(istream& in){
in >> name >> mid >> final;
return in;
}
double info::add() const {
return ::add(mid, final);//调用非成员函数
}
double add(double mid, double final){//如果成绩为零,报错
if(mid == 0 || final == 0){
throw domain_error("grade is null");
}
return add(deal(mid,final));
}
double deal(double mid, double final){//计算总成绩
int grade = mid * 0.3 + final* 0.7;
return grade;
}
double add(double grade){//把150分制的成绩转为100分制
int final_grade = grade/150*100;
return final_grade;
}
int main(int argc, char** argv)
{
info record;
record.read(cin);
cout << record.add();
return 0;
}
2.3 非成员函数
暂空
2.4 结构和类
结构struct和类class对成员的默认保护方式是不同的。
如果使用class,那么在第一个{和第一个保护标识符之间的全部成员都是私有的,
使用struct,那么在第一个{和第一个保护标识符之间的全部成员都是公有的,
保护标识符:public的成员是可以访问的,private的成员仅仅对类的成员才可以访问。
保护标识符可以以任何顺序出现,同时也可以出现多次。
例如
class info{
public:
double grade;
};
等价于
struct{
double grade;
};
另外:
class info{
std::string name;//默认私有
//其他私有成员
public :
double add() const;
//其他公有成员
};
等价于
struct info{
private:
std::string name;
//其他私有成员
public:
double add() const;
//其他公有成员
}
对于一个结构或类可以做相同的的处理操作,除非阅读代码,否则用户无法辨别。
对于类或结构的选择可以对程序设计起到很好的提示作用,通常来讲,程序设计风格是保留结构以指示简单的类型,并且我们希望公开这些类的数据结构。
3 设计类的常用函数
3.1 存取器函数
存取器函数:允许我们对一部分数据结构的读访问,但是不允许写访问。
我们经常利用这样的数据结构对数据进行简单的访问,但是这回破坏程序的封装性。
下面程序中,除了info的对象成员,其他函数的无法访问对象中的name。
calss info{
public:
istream& read(std::istream&);
double add() const;
private:
std::string name;
double grade;
};
为了能访问信息中的姓名,我们编写一个存取器函数:
calss info{
public:
istream& read(std::istream&);
double add() const;
std::string name() const {return n;}//存取器函数
private:
std::string n;
double grade;
};
3.2 构造函数
构造函数:定义了对象的初始化方式。
一个构造函数不能被显示调用,相反,在创建一个自定义类型的对象时,作为其副作用,一个适当的构造函数会被调用。
例如:在定义字符串或向量时,如果不指定一个初始值,那会得到一个空字符串或向量。string与vector类型都允许我们给一个新对象指定一个初值,例如,我们可以指定一个长度或指定一个数量以及一个填充的字符。
3.2.1 是否定义一个构造函数
良好的习惯:确保全部的数据成员在任何时候都具有有意义的值。便于以后(程序员或代码维护人员)增加的检查数据 成员的操作不会失败。
如果没有定义任何构造函数,编译程序就会为我们合成一个。合成的构造函数将会初始化数据成员,成员的初始值取决于对象的创建方式。
如果对象是一个局部变量。数据成员将会被默认初始化。
如果是下面三种情况,数据成员将会被数值初始化。
- 1 对象被用于初始化一个容器元素;
- 2 为映射表添加一个新元素,而对象是这个添加动作的副作用;
- 3 定义一个有特定长度的容器,对象是这个容器的元素。
总结起来:
- 1 如果对象是内部类型,数值初始化方式会将它设为零,而默认初始化方式会给它一个为定义的值。
- 2 如果对象属于自定义类型,而这种自定义类型定义了一个或多个构造函数,那么合适的构造函数就会完全控制对类的对象的初始化。
- 3 否则,对象只能是属于未定义任何构造函数的自定义类型。在这种情况下,对该对象的数值或默认初始化操作会对它的每一个数据成员进行相应的数值或默认初始化。如果有任何一个数据成员属于一种本身具有构造函数的自定义类型,那这个初始化过程都将会是递归的。
在下面的示例程序中,info类属于第三种情况,自定义类型,没有构造函数。
如果我们定义了一个info变量,n将会被初始化为一个空字符串,mid、final将会被初始化为一个未定义的值,这意味着它们将保存在它们创建时所获得的内存区域中的任何无用的值。这将会导致后面定义的一些操作失效。
calss info{
public:
istream& read(std::istream&);
double add() const;
std::string name() const {return n;}//存取器函数
private:
std::string n;
double mid, final;
};
实际上我们希望定一个两个构造函数,一个不参数,它创建一个空的info对象;第二个构造函数,具有一个对输入流的引用,从流中读入一条信息记录,从而初始化这个对象。
完善后的类:
calss info{
public:
info();//创建一个空的info对象
info(std::istream&);//读入一个流从而构造一个对象
istream& read(std::istream&);
double add() const;
std::string name() const {return n;}//存取器函数
private:
std::string n;
double mid, final;
};
3.2.2 默认构造函数
默认构造函数:不带任何参数的构造函数。
它的工作是确保对象的全部数据成员被正确的初始化。
创建新的类对象时,会进行的步骤:
- 1 实现分配内存以保存这个对象;
- 2 按照构造函数初始化程序列表而对对象进行初始化;
- 3 执行构造函数的函数体。
实现一个对象会初始化对象的全部数据成员。无论这些成员有没有构造函数的初始化列表中出现,构造函数的函数体可能会随后改变这些值,不过初始化程序列表会在构造函数的函数体之前发生。
通常,构造函数的函数体中为一个成员初始化并不是非常理想的操作,更好的是为成员明确的指定一个初始化值。除了我们要使用一个类成员初始化另一个类成员时,为了避免相互依赖,应该在构造函数的函数体内给这些成员赋值,而不应该在构造函数初始化程序中初始化它们。
原因有:
- 1 有些情况必须使用初始化程序列表。
- 成员是引用数据成员,常量数据成员和对象数据成员时,不能被赋值,只能被初始化。
- 2 效率。
- 在使用函数体内初始化时,一般会同时调用构造函数和复制操作符函数,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。
- 使用初始化列表只会调用一次构造函数。
注意:在使用初始化程序列表时,要按照类中类成员的声明顺序进行初始化,而不是按照初始化列表中的顺序。
对于前面的信息类,我们希望初始化数据表示我们还没有读到记录。
下面的程序中,
:和{之间是构造函数的初始化程序,它们命令编译程序初始化给定的成员,并且在初始化的时候使用出现在相应的括号之间的值。
mid和final被显示初始化为0,而n将会被隐式初始化。
info::info():mid(0),final(0){ }
3.2.3 带参数的构造函数
一般用于读入数据进行数据成员初始化。