C++(19)类与数据抽象
2015-08-08 19:41
495 查看
类
--类的定义和声明【上】
引言:在C++中,用类来定义自己的抽象数据类型。通过定义类型来对应所要解决的问题中的各种概念,可以使我们更容易编写、调试和修改程序。可以使得自己定义的数据类型用起来与内置类型一样容易和直观。
一个前面曾经定义过的类:
[cpp] view
plaincopy
class Sales_item
{
private:
std::string isbn;
unsigned units_sold;
double revenue;
public:
double ave_price() const;
bool same_isbn(const Sales_item &rhs) const
{
return isbn == rhs.isbn;
}
Sales_item():units_sold(0),revenue(0) {}
};
double Sales_item::ave_price() const
{
if (!revenue)
{
return 0;
}
return revenue / units_sold;
}
一、类定义:扼要重述
简单来说,类就是定义了一个新的类型和一个新的作用域。
1、类成员
访问标号:在public部分定义的成员可被使用该类型的所有代码访问:在private部分定义的成员可被其他类成员访问。
所有成员必须在类的内部声明,一旦类定义完成之后,就没有任何方式可以增加成员了。
2、构造函数
在创建一个类对象时,编译器会自动使用一个构造函数来初始化该对象,也就是说:构造函数用于给每个数据成员设定适当的值。
构造函数一般使用一个构造函数初始化列表,来初始化对象的数据成员。
[cpp] view
plaincopy
Sales_item():units_sold(0),revenue(0) {}
构造函数初始化类表由成员名和带括号的初始值组成,跟在构造函数的形参表之后,并以冒号(:)开头。
3、成员函数
在类内部定义的函数默认为内联函数(inline)。
而在类的外部定义的函数必须指明它们是在类的作用域中。
就const关键字加在形参表之后,就可以将成员函数声明为常量:
[cpp] view
plaincopy
bool same_isbn(const Sales_item &rhs) const;
const必须同时出现在声明和定义中,若只出现在一处,则会出现编译时错误!
[cpp] view
plaincopy
//类内
bool same_isbn(const Sales_item &rhs) const;
//类外
bool Sales_item::same_isbn(const Sales_item &rhs) //Error
{
return isbn == rhs.isbn;
}
[cpp] view
plaincopy
//P369 习题12.1、2、3、4
class Person
{
public:
Person(const std::string &Name,const std::string &Address):name(Name),address(Address){}
std::string getName() const;
std::string getAddress() const;
private:
std::string name;
std::string address;
};
std::string Person::getName() const
{
return name;
}
std::string Person::getAddress() const
{
return name;
}
二、数据抽象和封装
数据抽象是一种依赖于接口和实现分离的编程技术:类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。
封装是一项将低层次的元素组合起来形成新的、高层次实体的技术!函数和类都是封装的具体形式。其中类代表若干成员的聚集。大多数(设计良好的)类类型隐藏了实现该类型的成员。
1、访问标号实施抽象和封装
1)程序的所有部分都可以访问带有public标号的成员。类型的数据抽象视图由其 public成员定义。
2)使用类的代码不可以访问带有private标号的成员。private封装了类型的实现细节。
【class与struct的区别:】
如果类是用struct关键字定义的,则在第一个访问标号之前的成员是公有的;如果是用class定义,则在第一个访问标号之前的成员是私有的!
【建议:具体类型和抽象类型】
并非所有类型都必须是抽象的。标准库中的pair类就是一个实用的、设计良好的具体类而不是抽象类,具体类会暴露而非隐藏其实现细节。
但是具体类通常还有成员函数。特别地,如果类具有内置类型或复合类型数据成员,那么定义构造函数来初始化这些成员就是一个好主意。类的使用都也可以初始化或赋值数据成员,但由类来做更不易出错。
2、编程角色的不同类别
好的类的设计者会定义直观和易用的类接口,而用户(此处可以指程序员)则只需关心类中影响他们使用的那部分实现。
【注意,C++程序员经常会将应用程序的用户和类的使用者都称为“用户”。】
在简单的应用程序中,类的使用者和设计者也许是同一个人。即使在这种情况下,保持角色的区分也是有益的。设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类是如何工作的!
【关键概念:数据抽象和封装的好处】
1)避免类内部出现无意的、可能破坏对象状态的用户级错误。
2)随着时间的推移,可以根据需求改变或缺陷报告来完善类实现,而无需改变用户级代码。
【P371值得仔细品读!】
【注解:】
改变头文件中的类定义可有效地改变包含该头文件的每个源文件的程序文本,所以,当类发生改变时,使用该类的代码必须重新编译。
三、关于类定义的更多内容
C++语言为类提供了相当多的支持【这从C++的原名:CWith Class,就可看出...】
1、同一类型的多个数据成员
类的数据成员的声明类似于普通变量的声明:
[cpp] view
plaincopy
class Screen
{
public:
//...
private:
std::string contents;
std::string::size_type cursor;
std::string::size_type height,width;
};
2、使用类型别名来简化类
出了定义数据和函数成员之外,类还可以定义自己的局部类型名字。如果为std::string::size_type提供一个类型别名,那么Screen类将是一个更好的抽象:
[cpp] view
plaincopy
class Screen
{
public:
//...
typedef std::string::size_type index;
private:
std::string contents;
index cursor;
index height,width;
};
将index定义放在public部分,是因为希望用户使用这个名字!
3、成员函数可以被重载
成员函数只能重载本类的其他成员函数。
重载的成员函数和普通函数应用相同的规则:两个重载成员的形参数量和类型不能完全相同。调用非成员重载函数所用到的函数匹配过程也应用于重载成员函数的调用。
4、定义重载函数
[cpp] view
plaincopy
class Screen
{
public:
typedef std::string::size_type index;
//一下两个函数构成重载
char get() const
{
return contents[cursor];
}
char get(index ht,index wd) const;
private:
std::string contents;
index cursor;
index height,width;
};
使用:
[cpp] view
plaincopy
Screen myScreen;
char ch = myScreen.get();
ch = myScreen.get(0,0);
5、显式指定inline函数
在类内部定义的成员函数,默认就是inline函数。但是也可以显式的将成员函数指定为inline:
[cpp] view
plaincopy
class Screen
{
public:
typedef std::string::size_type index;
//默认就是inline函数
char get() const
{
return contents[cursor];
}
inline char get(index ht,index wd) const;
index get_cursor() const;
private:
std::string contents;
index cursor;
index height,width;
};
//已经在类体中声明为inline了,就没必要再次声明
char Screen::get(index r,index c) const
{
index row = r * width;
return contents[row + c];
}
//即使没有在类体中声明,也可以在外面补上...
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
在声明和定义处指定inline都是合法的。在类的外部定义inline的一个好处是可以使得类比较容易阅读。
【最佳实践】
像其他inline一样,inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。
类
--类的定义和声明【下】
三、关于类定义的更多内容【接上】[cpp] view
plaincopy
//P374 习题12.8
class Sales_item
{
public:
double avg_price() const;
bool same_isbn(const Sales_item &rhs) const
{
return rhs.isbn == isbn;
}
Sales_item():units_sold(0),revenue(0){}
private:
std::string isbn;
unsigned units_sold;
double revenue;
};
double Sales_item::avg_price() const
{
if (units_sold)
{
return revenue / units_sold;
}
return 0;
}
[cpp] view
plaincopy
//习题12.9
class Screen
{
public:
typedef std::string::size_type index;
char get() const
{
return contents[cursor];
}
inline char get(index ht,index wd) const;
index get_cursor() const;
Screen(index ht,index wd,std::string Cont):height(ht),width(wd),contents(Cont) {}
private:
std::string contents;
index cursor;
index height,width;
};
四、类声明和类定义
在一个给定的源文件中,一个类只能被定义一次。如果在多个文件中定义一个类,那么每个文件中的定义必须是完全相同的。
可以声明一个类而不定义它:
[cpp] view
plaincopy
//前向声明
class Screen;
在声明之后、定义之前,类Screen是一个不完全类型,即:已知Screen是一个类型,但不知道包含哪些成员。
不完全类型(incompletetype)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。
在创建类的对象之前,必须完整的定义该类。必须是定义类,而不是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样的,在使用引用或指针访问类的成员之前,必须已经定义类。
为类的成员使用类声明:
只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员只能是指向该类类型的指针或引用。
因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用:
[cpp] view
plaincopy
class LinkScreen
{
Screen Window;
LinkScreen *next;
LinkScreen *prev;
};
类的前身声明一般用来编写相互依赖的类。
[cpp] view
plaincopy
//P375 习题12.11
class Y;
class X
{
Y *y;
};
class Y
{
X x;
};
五、类对象
定义一个类时,也就是定义了一个类型。一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配内存空间,但(一般而言)定义类型时不进行存储分配。
通过类定义出的对象都具有自己的类数据成员的副本。修改其中一个对象不会改变其他该类对象的数据成员。
1、定义类类型的对象
定义了一个类类型之后,可以按以下两种方式使用。
1)将类的名字直接用作类型名。
2)指定关键字class或struct,后面跟着类的名字:
[cpp] view
plaincopy
Screen scr;
//两条语句作用相同
class Screen scr;
2、为啥类的定义以分号结束
因为在类定义之后可以接一个对象定义列表,所以,定义必须以分号结束:
[cpp] view
plaincopy
class Sales_item
{
//...
};
class Sales_item
{
//...
} ccum,trans;
【最佳实践:】
通常,将对象定义成类定义的一部分是个坏主意!!!这样做,会使所发生的操作难以理解。对读者而言,将两个不同的实体(类和变量)组合在一个语句中,也会令人迷惑不解。
类
--隐含的this指针
引言:在前面提到过,成员函数具有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含形参命名为this,与调用成员函数的对象绑定在一起。成员函数不能定义this形参,而是有编译器隐含地定义。成员函数可以显式的使用this指针,但不是必须这么做。
1、何时使用this指针
有一种情况下,我们必须显式使用this指针:当需要将一个对象作为整体引用而不是引用对象的一个成员时。
比如在类Screen中定义两个操作:set和move,可以使得用户将这些操作的序列连接成一个单独的表达式:
[cpp] view
plaincopy
myScreen.move(4,0).set('#');
//其等价于
myScreen.move(4.0);
myScreen.set('#');
2、返回*this
在单个表达式中调用move和 set操作时,每个操作必须返回一个引用,该引用指向执行操作的那个对象:
[cpp] view
plaincopy
class Screen
{
public:
Screen &move(index r,index c);
Screen &set(char);
Screen &set(index,index,char);
};
这样,每个函数都会返回调用自己的那个对象。使用this指针可以用来访问该对象:
[cpp] view
plaincopy
Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
Screen &Screen::move(index r,index c)
{
index row = r * width;
cursor = row + c;
return *this;
}
3、从const成员返回*this
在普通的非const成员函数中,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
不能从const成员函数返回指向类对象的普通引用。const成
员函数只能返回*this作为一个 const引用。
我们可以给Screen类增加一个const成员函数:display操作。如果将display作为 Screen的 const成员,则 display内部的 this指针将是一个constScreen* 型的const。然而:
[cpp] view
plaincopy
myScreen.move(4,0).set('#').display(cout); //OK
myScreen.display().set('*'); //Error
问题在于这个表达式是在由display返回的对象上运行set。该对象是const,因为display将其对象作为const返回。我们不能在const对象上调用set。
4、基于const的重载
为了解决以上问题,我们必须定义两个display操作:一个是const,一个不是const。基于成员函数是否为const,可以重载一个成员函数;同样的,基于一个指针形参是否指向const,也可以重载一个函数。非const对象可以使用任一成员,但非const版本是一个更好的匹配。
[cpp] view
plaincopy
class Screen
{
public:
//As before
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;
}
//As before
};
调用:
[cpp] view
plaincopy
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(cout); //调用非const版本
blank.display(cout); //调用const版本
5、可变数据成员
有时,我们希望类的数据成员(甚至是在const成员函数中)可以修改。这可以通过将它们声明为mutable来实现。
可变数据成员永远都不能为const,甚至当它们是const对象的成员时也如此。因此,const成员函数可以改变mutable成员。
[cpp] view
plaincopy
class Screen
{
public:
//...
private:
mutable size_t access_ctr;
//使用access_ctr来跟踪Screen成员函数的调用频度
void do_display(std::ostream &os) const
{
++ access_ctr; //OK
os << contents;
}
};
【建议:用于公共代码的私有实用函数】
使用私有实用函数(如前面的do_display)的好处:
1)一般愿望是避免在多个地方编写同样的代码。
2)display操作预期会随着类的演变而变得复杂。当涉及到的动作变得更复杂时,只在一处而不是两处编写这些动作有更显著的意义。
3)很可能我们会希望在开发时给do_display增加调试信息,这些调试信息将会在代码的最终成品版本中去掉。如果只需要改变一个do_display的定义来增加或删除调试代码,这样做将更容易。
4)这个额外的函数调用不需要涉及任何开销。我们使do_display成为内联的,所以调用do_display与将代码直接放入display操作的运行时性能应该是相同的。
P379习题12.13
[cpp] view
plaincopy
//1. in screen.h
#ifndef SCREEN_H_INCLUDED
#define SCREEN_H_INCLUDED
#include <string>
class Screen
{
public:
typedef std::string::size_type index;
Screen &move(index r,index c);
Screen &set(char);
Screen &set(index,index,char);
char get() const
{
return contents[cursor];
}
char get(index ht,index wd) const;
index get_cursor() const;
Screen &display(std::ostream &os)
{
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const
{
do_display(os);
return *this;
}
Screen():cursor(0),height(0),width(0){}
Screen(index,index,const std::string &tmp);
private:
void do_display(std::ostream &os) const
{
os << contents;
}
std::string contents;
index cursor;
index height,width;
};
inline char Screen::get(index ht,index wd) const
{
index row = width * ht;
return contents[row + wd];
}
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
#endif // SCREEN_H_INCLUDED
[cpp] view
plaincopy
//2. in screen.cpp
#include "screen.h"
Screen::Screen(index ht,index wd,const std::string &cntnts):height(ht),width(wd),cursor(0),contents(cntnts){}
Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
Screen &Screen::set(index ht,index wd,char c)
{
index row = ht * width;
contents[row + wd] = c;
return *this;
}
Screen &Screen::move(index r,index c)
{
index row = r * width;
cursor = row + c;
return *this;
}
[cpp] view
plaincopy
//3. in main.cpp
#include <iostream>
#include "screen.h"
using namespace std;
int main()
{
Screen myScreen(5,6,"aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n");
myScreen.move(4,0).set('#').display(cout);
}
类
--类作用域
引言:每个类都定义了自己的新作用域与唯一的类型。即使两个类具有完全相同的成员列表,它们也是不同的类型。每个类的成员不同与任何其他类(或任何其他作用域)的成员。
[cpp] view
plaincopy
class First
{
public:
int memi;
double memd;
};
class Second
{
public:
int memi;
double memd;
};
First obj1;
Second obj2 = obj1; //Error
1、使用类的成员
在类作用域之外,成员只能通过对象或指针使用成员访问操作符.或->来访问。这些操作符左边的操作数分别是一个类对象或指向类对象的指针。
还有一些直接通过类使用作用域操作符(::)来访问。如:定义类型的成员,Screen::index,使用作用域操作符来访问。
2、作用域与成员定义
尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样。
[cpp] view
plaincopy
//一旦看到成员的完全限定名,就知道该类定义是在类作用域中
double Sales_item::avg_price() const
{
if (units_sold)
{
return revenue / units_sold;
}
return 0;
}
3、形参表和函数体处于类作用域中
在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
[cpp] view
plaincopy
char Screen::get(index r,index c) const
{
index row = r * width;
return contents[row + c];
}
4、函数返回类型不一定在类作用域中
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。
[cpp] view
plaincopy
class Screen
{
public:
typedef std::string::size_type index;
index get_cursor() const;
//...
};
//由于index出现在函数名被处理之前,所以代码不在类作用域内。
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
[cpp] view
plaincopy
//P381 习题2.16
//error: 'index' does not name a type
index Screen::get_cursor() const
{
return cursor;
}
类作用域中的名字查找
一直以来,我们程序的名字查找:
1)首先,在使用该名字的块中查找名字的声明。只考虑在该项使用之前声明的名字。
2)如果在1)中找不到,则在包围的作用域中查找。
如果找不到任何声明,则出错。
类的定义实际上是在两个阶段中处理:
1)首先,编译器声明;
2)只有在所有成员出现之后,才编译他们的定义本身。
1、类成员声明的名字查找
按照以下方式确定在类成员的声明中用到的名字:
1)检查出现在名字使用之前的类成员的声明。
2)第一步查找不成功,则价差包含类定义的作用域中出现的声明以及出现在类定义之前的声明!
如:
[cpp] view
plaincopy
typedef double Money;
class Account
{
public:
Money balance()
{
return bal;
}
private:
Money bal;
};
【谨记:】
必须是在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
一旦一个名字被用作类型名,则该名字就不能被重复定义:
[cpp] view
plaincopy
typedef double Money;
class Account
{
public:
Money balance()
{
return bal;
}
private:
typedef long double Money; //Error
Money bal;
};
2、类成员定于中的名字查找
1)首先检查成员函数局部作用域中的声明。
2)如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
3)如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明。
3、类成员遵循常规的块作用域名字查找
在程序中尽量不要使用相同名字的形参和成员:
[cpp] view
plaincopy
int height;
class Screen
{
public:
typedef int index;
//由于在函数形参中定义了height,
//所以此处的height形参会屏蔽名为height的成员
void dummy_fcn(index height)
{
cursor = width * height;
}
private:
index cursor;
index height,width;
};
尽管类的成员被屏蔽了,但是仍然可以通过使用类名来限定成员名或显式使用this指针来使用:
[cpp] view
plaincopy
void dummy_fcn(index height)
{
cursor = width * this -> height;
//这两条语句作用相同
cursor = width * Screen::height;
}
4、函数作用域之后,在类作用域中查找
如果想要使用height成员,最好的方式还是为形参定义一个不同的名字:
[cpp] view
plaincopy
void dummy_fcn(index ht)
{
cursor = width * height;
}
尽管height是先在dummy_fcn中使用,然后再声明,但是编译器还是确定这里用的是名为height的数据成员。
5、类作用域之后,在外围作用域中查找
尽管全局对象height在类中被数据成员height屏蔽了,但还可以通过全局作用域确定操作符来限定名字,仍然可使用它:
[cpp] view
plaincopy
void dummy_fcn(index height)
{
cursor = width * ::height
}
6、在文件中名字的出现初确定名字
当成员定义在类定义的外部时,名字查找的第3步不仅要考虑在Screen类定义之前的全局作用域中的声明,而且要考虑在成员函数定义之前出现的全局作用域声明:
[cpp] view
plaincopy
class Screen
{
public:
typedef int index;
void setHeight(index);
private:
index height;
};
Screen::index verify(Screen::index);
void Screen::setHeight(index val)
{
/*
*val由形参定义
*verify是全局函数
*height是类的数据成员
*/
height = verify(val);
}
[cpp] view
plaincopy
//P385 习题12.17
//编译查看错误
class Screen
{
public:
void setHeight(index);
private:
index height;
typedef int index;
};
[cpp] view
plaincopy
//习题12.18
typedef string Type;
Type initVal();
class Exercise
{
public:
Type setVal(Type);
private:
Type val; //string val;
};
Type Exercise::setVal(Type parm)
{
val = parm + initVal();
return val;
}
相关文章推荐
- c++中的重载
- 关于C语言中的存储区划分以及相关内容
- c/c++下 long long与__int64不同编译环境的比较
- 【C++】智能指针的实现
- C/C++ 数据范围(int ,char long, float,double)
- 【C++基金会 04】vector详细解释
- 排序算法的C语言实现-冒泡排序
- CZY的组合数烦恼
- 排序算法的C语言实现-插入排序
- C++ Map 的使用
- C++设计模式系列
- C++中虚函数的作用及用法
- C++设计模式——原型模式
- C++_运算符重载
- C++运算符重载(二)——使用方法
- C++(18)再谈指针
- Microsoft Visual C++ 9.0 is required Unable to find vcvarsall.bat 解决办法
- C++编写简易的飞机大战
- C++中引用(&)的用法和应用实例
- C、C++用指针引用的差异