面向对象编程OOP特性:
1.抽象
2.封装和数据隐藏
3.多态
4.继承
5.代码可重用性
10.2 抽象和类
指定基本类型,比如int i,完成了3项工作:
1.决定数据对象需要的内存数量
2.决定如何解释内存中的位
3.决定可用数据对象执行的操作或方法
对于内置类型,这些操作内置到编译器,新创建类型,这些工作需要自己完成。
新建类标准流程是,类定义放在头文件,类方法实现放在源代码文件。
其中,private和public用于描述访问控制。任何类对象的程序都可以访问public部分,但是只有通过public成员才能访问到private成员。protected用于继承,以后介绍。
类中的成员默认是private的,这和结构不同,结构中默认成员是public的
类成员函数定义的地方要使用::表示类作用域,比如public中声明的buy函数,实现的时候应该这样写
void Stock::buy(long num, double)
{
}
这个规则让多个类中可以有同名成员函数。Stock::buy()是函数限定名,buy()是非限定名,只能在类作用域中使用。
若某函数在类中作为private成员给出定义,会自动成为内联函数,将其作为内联函数的原因是,此函数是实现代码的一部分,而不是公有接口的一部分。内联函数的特殊要求是,每个调用内联函数的文件都必须有此函数的定义,因此内联函数必须放入头文件中。
创建对象和调用函数的方式如下:
Stock tmp;
tmp.show();
调用成员函数时,使用用来调用的对象的数据。每个新对象都有自己的存储空间,用于存储其内部变量和类成员,但是同一个类所有对象共享同一组类方法,即每种方法只有一个副本。调用成员函数就是给对象发送消息的方法。
类应该被设计成和内置类型有相同的功能,比如可以将一个对象赋给另一个,具有deep copy功能。
构造函数没有返回值,也没有声明类型,不能也没有必要强加否则会编译错误。原型必须在public部分,否则创建新对象的时候会报错。构造函数必须和类名同名。每次创建对象(静态或者动态),自动调用构造函数,构造函数只能用做初始化,不能用过对象调用。
默认构造函数用于未提供显示初始值的时候初始化对象使用,让类能够做到类似int i这样的事情。若没有定义任何构造函数,那么编译器提供默认构造函数,没有任何参数值和内容,形如
Sock::Sock()
{
}
若定义了一个构造函数,那么编译器就不会提供默认构造函数了,这时候需要手动定义,有两种方式,一种是给已有的构造函数加上默认参数,另一种是额外加一个类似编译器提供的默认构造函数,两种方式如下:
/* 使用额外的无参数默认构造函数 */
class Test
{
private:
int a;
int b;
public:
Test(){}
Test(int, int);
~Test(){}
};
Test::Test(int _a, int _b)
{
int a = _a;
int b = _b;
}
/* 在已有的构造函数基础上,增加默认参数 */
class Test
{
private:
int a;
int b;
public:
Test(int = 0, int = 0);
~Test(){}
};
Test::Test(int _a, int _b)
{
a = _a;
b = _b;
}
注意,使用第二种方式,即默认参数的构造函数的时候,必须给每个参数提供默认值,否则不会被认为是默认构造函数,一般情况下,使用第一种方式,即增加一个无参数的默认构造函数,如下:
class Test
{
private:
int a;
int b;
public:
Test(){} //默认构造函数
Test(int, int = 0){} //显式初始值的构造函数
~Test(){}
};
注意,显式初始值的构造函数可以给除了第一个参数以外的其他参数提供默认初始值,因为若没有初始值,那么一定调用的是默认构造函数,若有初始值,那么第一个参数一定是给出确定的初始值的,所以可以这样写。
析构函数在对象过期时自动调用,若没提供显式定义的析构函数,那么编译器生成一个隐式析构函数,如下:
Test::~Test()
{
}
和构造函数不同,析构函数只能有一个,且没有参数。除了定位new分配内存等特殊情况,不应该显式调用析构函数,而是应该让编译器决定调用析构函数的时机。
一个构造函数和析构函数时机的例子:
/* 类声明 */
class Stock
{
private:
int i;
int j;
public:
Stock();
Stock(int, int = 0);
~Stock();
void show() const;
};
/* 方法实现,为了看调用时机,只打印,不做实际操作 */
Stock::Stock()
{
cout << "Stock::Stock()" << endl;
}
Stock::Stock(int i, int j)
{
cout << "Stock::Stock(int i, int j)" << endl;
}
Stock::~Stock()
{
cout << "~Stock()" << endl;
}
void Stock::show() const
{
cout << "show()" << endl;
}
int main()
{
cout << "-----------------" << endl;
/* 构造stock1和stock2 */
Stock stock1(1, 2);
Stock stock2 = Stock(1, 2);
/* 这一步是单纯的赋值,没有构造或者析构的调用 */
stock2 = stock1;
cout << "-----------------" << endl;
/* 构造一个临时对象,赋值给stock1,然后析构这个临时对象 */
stock1 = Stock(1, 2);
cout << "-----------------" << endl;
return 0;
}
/* main大括号结束之后,析构两次,变量存在栈中,因此先stock2然后stock1 */
声明对象的时候直接初始化和先用默认构造函数声明以后再初始化有一个本质的区别,就是后续初始化的时候,下面这个语句会创建一个临时对象,相当于多了一次构造和析构,有开销,因此应该尽可能声明时初始化
Stock stock1;
stock1 = Stock(1, 2);
c++11提供了列表初始化方法。
对于const对象,若调用show()函数,编译器会报错,因为不能保证内部不修改对象的成员。普通函数通过给参数加const解决,但是对于类来说,参数是对象的数据成员而不是通过函数列表提供的,因此需要在show()后面加const
若构造函数只有一个参数,将对象初始化为一个与参数类型相同值的时候,该构造函数被调用,比如:
class Test
{
private:
int i;
public:
Test(int){}
~Test(){}
};
int main()
{
Test tmp = 1;
}
这种特定可能导致问题,可以被关闭,在chapter11中介绍。
需要比较两个对象的数据成员的值的时候,需要声明如下函数:
const Stock & compare(const Stock & co) const;
传入一个Stock类对象的引用,此对象不能在此函数中修改所以加const,返回值是一个const Stock &的引用,不修改调用对象的数据,因此最后加const。这个函数隐式访问一个对象,显示访问一个对象,形如
stock1.compare(stock2)
的调用,隐式访问stock1,显式访问stock2
此函数的问题是,stock2显式提供了,但是stock1是隐式的,函数内部无法表示stock1,因此使用this指针解决这个问题。this指针的默认类型是Stock * const,即不能修改this指针指向的位置,函数后面加const之后,this指针类型变成Stock const * const。
对象数组中,未被显式初始化的元素调用默认构造函数,比如对于如下声明:
Stock a[3] = {Stock(1, 2)};
除了第一个元素之外,其余元素调用默认构造函数
向声明作用域为类的符号常量,不能直接声明,比如下面这种方式:
class Test
{
private:
const int Month = 12;
double a[Month];
public:
Test(){}
~Test(){}
};
编译会报错,因为类声明只是描述,没有创建对象,在创建对象之前,是没有存储空间的,没有地方可以存放Month,c++11提供了成员初始化,允许const int Month = 12通过编译,但是double a[Month]无法通过编译,因此尽管Month被声明为const,仍不是静态的,在c++11中,只有如下声明能通过编译,同时也适用于早期c++版本,Month存储在静态变量区,而不是对象中,因此所有对象共享:
class Test
{
private:
static const int Month = 12;
double a[Month];
public:
Test(){}
~Test(){}
};
早期c++版本中,可以使用枚举,创建作用域是类的符号常量,如下:
class Test
{
private:
enum {Month = 12};
double a[Month];
public:
Test(){}
~Test(){}
};
用这种方法,也不会创建类数据成员,即所有对象中都不包括这个枚举,Month只是一个符号名称,编译器在类作用域中遇到的时候直接用12代替了。
作用域内枚举:c++11提供,用于解决传统枚举问题,比如:
enum egg
{
Small,
Medium,
Large,
Jumbo
};
enum t_shirt
{
Small,
Medium,
Large,
Xlarge
};
同名枚举位于相同作用域,因此名称冲突。c++11提供的方式如下:
enum class egg
{
Small,
Medium,
Large,
Jumbo
};
enum class t_shirt
{
Small,
Medium,
Large,
Xlarge
};
egg choice = egg::Large;
c++11作用域内枚举的特点是,不能隐式提升为int,比如如下赋值
int i = egg::small;
会编译报错,需要时需要强制转换,还有一个区别是,c++98中枚举的底层整数类型取决于实现,c++11规定为int,在这里class可以用struct替换