以下纯纯为个人理解
过程性编程——将整体的数据单独考虑,通过单独的数据来设计函数达到自己的目的(考虑对象对单独的数据)
面向对象的编程——将整体的数据做成一个整体,对于这个整体进行考虑(把整体当成一个新的单独个体来考虑我要对它做什么 初始化,计算,显示和更新等),通过实现对整体的操作来达到目的(考虑对象为整体的东西)
对于数据,我们想要对它分类,可以根据数据的外观对其进行分类(在内存中如何存储,char占1个字节,int占4个字节等),但是,我们也可以根据对它执行的操作来进行分类。例如,对于指针和int数据,它们在内存中可能都占4个字节,但是int数据我们可以对其进行加减乘除取余操作,但是对于指针数据,便不能进行乘运算,因为没有意义,所以我们根据对数据的不同操作将int数据和指针数据进行了分类。
我们在声明变量时,不仅分配了内存,还规定了可对变量执行的操作。也就是说,指定基本类型完成了三项工作:
1)决定数据对象需要的内存数量;
2)决定了如何解释内存中的位(转换数值的方法不同,比如int和float)
3)决定了对数据对象执行的操作和方法
简单地说,类就是将数据表示和操作数据的方法组合成一个整洁的包。
例如,我们现在要定义一个表示股票的类。
我们首先考虑数据表示,也就是我们要如何表示股票。具体地说,我们将存储以下信息来表示股票:
1)公司名称;
2)所持股票的数量;
3)每股的价格;
4)股票总值
接下来我们考虑对股票所执行的操作:
1)获得股票;
2)增持;
3)卖出股票;
4)更新股票的价格;
5)显示关于所持股票的信息。
接下来定义类,一般来说,类规范由两部分组成。
1)类声明:数据成员的方式描述数据部分,以成员函数的方式描述公有接口。
2)类方法定义:描述如何实现成员函数
补充:什么是接口?
接口是一个框架,供两个系统交互时使用。对于类,我们说公共接口,它沟通了编写类的人和使用类的人(程序员,具体说是程序员设计的函数),接口具体来说可以当作对类的操作(成员函数)。
通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码中。
#ifndef STOCK00_H_
#define STOCK00_H_
#include
class stock //声明类
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void ste_tot() { total_val = shares * share_val; }
public:
void acquire(const std::string& co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif
在这个例子中,我们将stock类的声明放在头文件中,使用了#ifndef等来访问多次包含同一个文件。我们来看一下类的通用特性。
1.C++关键字class指出这些代码定义了一个类设计,stock是这个新类的类型名,我们可以通过stock来声明stock类型的变量——称为对象或实例。
stock sally;
stock solly;
2.要存储的数据以类数据成员形式出现,要执行操作以类函数成员形式出现。
3.private和public描述了对类成员的控制访问。使用类对象的程序 可以 直接访问公有部分,但只能通过友元函数(11章介绍)来访问对象的私有成员,这被称为数据隐藏(防止程序直接访问数据)。
4.无论是数据成员还是成员函数,均可以在私有和公有声明,但是由于隐藏数据是OOP主要目标之一,因此数据通常放在私有部分,组成类成员接口的成员函数放在公有部分。不需要在类声明中使用关键字private,因为默认就是private。这也是类和结构体最大区别,结构体默认是public。
#include
#include"stock00.h"
void stock::acquire(const std::string& co, long n, double pr) //函数头使用解析运算符
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
void stock::buy(long num, double price)
{
if (num < 0)
{
std::cout << "Number of shares purchased can't be negative;"
<< " Transaction is aborted.\n";
}
else
{
shares += num;
share_val = price;
set_tot();
}
}
void stock::sell(long num, double price)
{
using std::cout;
if (num < 0)
{
std::cout << "Number of shares sold can't be negative;"
<< " Transaction is aborted.\n";
}
else if (num > shares)
{
std::cout << "You can't sell more than you have!"
<< " Transaction is aborted.\n";
}
else
{
shares -= num;
share_val = price;
set_tot();
}
}
void stock::update(double price)
{
share_val = price;
set_tot();
}
void stock::show()
{
std::cout << "Company: " << company
<< "Shares: " << shares << '\n'
<< "Share Price:$ " << share_val
<< "Total Worth:$ " << total_val << '\n';
}
实现类成员函数需要注意的点:
1.定义成员函数时,使用作用域解析运算符来标识函数所属的类;——意味着我们可以在其他类中声明名称相同的函数;
2.类方法可以访问类的private组件(show函数)。
3.set_tot()只是实现代码的一种方式,而不是公有接口的组成部分,因此类将其声明为私有成员函数(定义在类声明的函数自动成为内联函数,在类声明中定义方法等同于用声明替换定义,然后在类声明的后面将定义改写为内联函数)。内联函数要求在每个使用它们的文件中都要对其定义,最简单的做法是将内联定义放在头文件中。
4.方法怎么使用对象?
和使用结构体成员一样,通过成员运算符:
stock kate, joe;
kate.show();
joe.show();
调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,方法被用于两个不同的对象。
#include
#include"stock00.h"
int main()
{
stock fluffy_the_cat;
fluffy_the_cat.acquire("NanoSmart", 20, 12.50);
fluffy_the_cat.show();
fluffy_the_cat.buy(15, 18.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(400, 20.00);
fluffy_the_cat.show();
fluffy_the_cat.buy(300000, 40.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(300000, 0.125);
fluffy_the_cat.show();
return 0;
}
使用类就和使用基本类型一样。要创建类,可以直接声明类变量,也可以使用new为类对象分配存储空间,可以将对象作为函数的参数和返回值。也可以将对象赋给另一个。
数字格式不一致,采用定点表示法来修改实现。修改实现仅需要在类成员函数实现里修改即可,保留头文件和客户文件不变(不能影响使用类的用户)。
对于上边的stock类,我们还有其他工作要做。上边的代码还不能狗让您像初始化int那样来初始化stock类,这是因为数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员,程序只能通过成员函数来访问。一般来说,最好再创建对象时对它进行初始化,为此,C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。构造函数名称和类的名称相同,虽然没有返回值,但是原型和函数头没有void关键字。
现在我们为stock类创建构造函数。
在头文件中的公有部分声明构造函数原型:
public:
void acquire(const std::string& co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
stock(const std::string& co, long n = 0, double pr = 0);
由于需要为stock对象提供三个值,因此应为构造函数提供三个参数。程序员可能只想设置company成员,而将其他值设为0,这可以使用默认参数来完成。
下面是构造函数的定义:
stock::stock(const std::string& co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
构造函数定义和acquire()相同,区别在于,程序声明对象时,将自动调用构造函数。
注意:
构造函数的参数不是类成员,而是要给赋给类成员的值,所以参数名不能与类成员相同,为了避免这种混乱,一种常见的做法是在数据成员名中使用m_前缀;或者在数据成员名后使用_后缀。
std::string m_company;
long shares_;
C++提供了两种使用构造函数来初始化对象的方式。
stock fluffy_the_cat = stock("world cabbage",250,1.25); //圆括号
stock fluffy_the_cat("world cabbage",250,1.25); //圆括号
每次创建对象,(包括使用new动态分配内存),C++都使用类构造函数。
stock* pstock = new stock("Furry Mason", 50, 2.5);
构造函数的使用方式不同于其他类方法。一般来说,使用对象来调用方法,但无法使用对象来调用构造函数,因为对象还没建立。
注:接受一个参数的构造函数允许使用赋值语句将对象初始化为一个值:
Bozo(int age);
Bozo tubby = 32;
(这种方法不是很好,11章将介绍如何关闭这项特性)。
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。
stock fluffy_the_cat;
如果没有提供任何构造函数,则C++将自动提供默认构造函数,默认构造函数没有参数,相当于创建变量未赋值。如果为类定义了构造函数,程序员就必须为他提供默认构造函数,如果提供了非默认构造函数,上边的声明将出错。
定义默认构造函数的方法有两种
stock(const std::string& co = "Error", long n = 0, double pr = 0);
stock::stock()
{
company = "no name";
shares = 0;
share_val = 0;
total_val = 0;
}
由于只能有一个默认构造函数,因此上边方式二选一,通常应初始化所有对象,以确保所有成员一开始有一个合理的值 。
注:隐式调用默认构造函数,不要使用圆括号。
对象过期时,程序将自动调用一个特殊的成员函数——析构函数。析构函数完成清理工作。
如果构造函数使用new来分配内存,则析构函数使用delete来释放这些内存;
如果构造函数没有使用new,析构函数没有需要完成的任务,只需要编译器生成一个什么都不要做的隐式析构函数即可。
析构函数的名称为 :在类的前边加上~。和构造函数一样,析构函数也可以没有返回值和声明类型,但是析构函数没有参数。
析构函数都是自动被调用的,如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除地代码后,提供默认析构函数地定义。
在类中加入构造函数和析构函数:
#ifndef STOCK00_H_
#define STOCK00_H_
#include
class stock //声明类
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
stock(const std::string& co , long n = 0, double pr = 0);
stock();
~stock();
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
//
};
#endif
类实现文件:
#include
#include"stock00.h"
void stock::buy(long num, double price)
{
if (num < 0)
{
std::cout << "Number of shares purchased can't be negative;"
<< " Transaction is aborted.\n";
}
else
{
shares += num;
share_val = price;
set_tot();
}
}
void stock::sell(long num, double price)
{
using std::cout;
if (num < 0)
{
std::cout << "Number of shares sold can't be negative;"
<< " Transaction is aborted.\n";
}
else if (num > shares)
{
std::cout << "You can't sell more than you have!"
<< " Transaction is aborted.\n";
}
else
{
shares -= num;
share_val = price;
set_tot();
}
}
void stock::update(double price)
{
share_val = price;
set_tot();
}
void stock::show()
{
std::cout << "Company: " << company
<< "Shares: " << shares << '\n'
<< "Share Price:$ " << share_val
<< "Total Worth:$ " << total_val << '\n';
}
stock::stock(const std::string& co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
stock::stock()
{
company = "no name";
shares = 0;
share_val = 0;
total_val = 0;
}
stock::~stock()
{
}
客户文件
#include
#include"stock00.h"
int main()
{
{
using std::cout;
cout << "Using constructors to create new objects\n";
stock stock1("NanoSmart", 12, 20.0);
stock1.show();
stock stock2 = stock("Boffo Objects", 2, 2.0);
stock2.show();
cout << "Assigning stock1 to stock2\n";
stock2 = stock1;
cout << "Listing stock1 and stock2\n";
stock1.show();
stock2.show();
cout << "Using a constructor to reset an object\n";
stock1 = stock("Nifty Food", 10, 50.0);
cout << "Revised stock1\n";
stock1.show();
cout << "Done!";
return 0;
}
}
解释:
1.创建并初始化新对象:
①创建新对象,并将数据成员初始化为指定的值;
②构造函数创建一个临时对象,然后将临时对象复制到要创建的新对象中,将临时对象丢弃(调用析构函数)。
2.对存在的对象赋新值:
构造函数创建一个临时对象,然后将临时对象复制到要创建的新对象中,将临时对象丢弃(调用析构函数)。
3.C++11列表初始化:
可以用大括号括起来对其进行初始化,注意,没有初始化的值参数默认设置为0;
4.const成员函数
const stock land = stock{ "Kludghsai Pdhsid" };
land.show();
第二行会报错,这是因为show()的代码无法确保调用对象不被修改,我们以前通过将函数参数声明为const引用或者指向const的指针来解决这个问题,但是这个函数没有参数,C++提供了新语法,将const关键字放在函数的括号后边。
void show() const;
void stock::show() const
{
std::cout << "Company: " << company
<< "Shares: " << shares << '\n'
<< "Share Price:$ " << share_val
<< "Total Worth:$ " << total_val << '\n';
}
这样来声明和定义函数,从现在开始,只要类方法不修改调用对象,就应该将其声明为const。
对于stock类,到目前为止,每个类成员都只涉及一个对象,即调用它的对象,但有时候方法可能涉及两个对象,这种情况下需要使用this指针。
假设我们现在需要设计一个函数,让程序查看股票,然后返回价值最高的那只股票。
由于程序无法直接访问数据,因此要让程序知道数据,最直接的方式就是让方法返回一个值,为此,通常使用内联代码来完成。
public:
double total() const { return total_val; }
我们还可以采用this指针的方法来找到价格最高的股票。
我们定义一个成员函数,它查看两个stock对象,并返回股价较高的那个对象的引用。将成员函数名命名为topval(),函数调用stock1.topval()将访问stock1对象的数据,如果要实现比较,则必须把第二个对象作为参数传递给函数。处于效率考虑,我们按引用传递;如何将方法的答案传递给程序呢?最简单的方法就是返回一个引用。问题在于,我们可以返回参数的引用,如果要返回该对象的引用,我们怎么返回呢?(stock1没有别名)
解决办法是:使用被称为this的特殊指针。this 指针指向用来调用成员函数的对象(作为隐藏参数传递给方法),这样当函数调用stock1.topval(stock2)时,this指针将设置为stock1的地址。
一般来说,所有的类方法都将this指针设置为调用它的对象的地址。
每个成员函数(包括构造函数和析构函数)都有一个this指针,如果要引用整个调用对象,返回*this(将*this作为调用对象的别名来完成前边方法的定义)。
头文件
public:
stock(const std::string& co , long n = 0, double pr = 0);
stock();
~stock();
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show() const;
const stock & topval(const stock& s) const;
const stock& stock::topval(const stock& s) const
{
if (s.total_val > total_val)
return s;
else
return *this;
}
我们可以单独创建多个对象,也可以创建一个对象数组。
初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。
stock mystuff[4] =
{
stock(),
stock("Ndhas",12.5,20),
stock{"Dfef",130,6.5}
}
声明是初始化了数组的部分元素,余下的元素将使用默认构造函数进行初始化。
#include
#include"stock00.h"
int main()
{
{
using std::cout;
stock mystuff[4] =
{
stock(),
stock("Ndhas",12.5,20),
stock{"Dfef",130,6.5},
stock("Fldei dhaio",60,6.5)
};
cout << "Stock holding:\n";
int st;
for (st = 0; st < 4; st++)
mystuff[st].show();
const stock* top = &mystuff[0];
for (st = 1; st < 4; st++)
top = &top->topval(mystuff[st]);
cout << "\nMost valuable holding:\n";
top->show();
return 0;
}
}
第九章,我们介绍了全局(文件)作用域和局部(代码块)作用域,函数名称的作用域只能是全局的(不然函数将无法被调用。)C++类引入了新的作用域:类作用域。
在类中定义的名称(类数据成员和类函数成员)的作用域都为整个类,在类外是不可见的,因此,可以在不同类中使用相同的类成员名而不会引起冲突。
类作用域意味着不能从外部直接访问类的成员,公有函数也是如此,也就是说,要调用共有函数,必须通过对象。在类声明或成员函数定义中,才可以使用未修饰的成员名称,构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名,必须根据上下文使用直接成员运算符、间接成员运算符或作用域解析运算符。
有时候,使符号常量的作用域为类很有用。该常量对于所有对象来说都是相同的,因此创建一个由所有对象共享的常量是一个很正常的想法。
但是如果直接在类private中定义const常量是行不通的,因为声明类只是描述了对象的形式,并没有创建对象,因此在创建对象前,将没有用于存储值的空间。有两种方式可以实现这个目标:
1.类中声明枚举
private:
enum {Months = 12};
double cost[Months];
注:这种方式声明枚举并不会创建类数据成员,只是编译器在遇到Months时,会用12来替换。由于这里使用枚举量只是为了创建符号常量,并不打算创建没哭类型变量,因此不需要提供枚举名。
2.另一种方式时使用关键字——static
static const int Months = 12;
double cost[Months];
这将创建一个名为Months的常量,该常量与其他静态变量存储在一起,而不是存储在对象里,只有一个Months常量,被所有对象共享。
传统的枚举,两个枚举定义中枚举量可能发生冲突,为了避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。枚举的声明类如下:
enum class egg {small,medium,large,jumbo};
enum struct t_shirt {small,medium,large,jumbo};
egg choice = egg::large;
t_shirt floyd = t_shirt::large;
也可使用关键字 struct代替class,无论哪种方式,都需要使用枚举名来限定枚举量(不然没法访问)。
C++11还提高了作用域内枚举的类型安全,在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于表达式时,但作用域内枚举不能隐式地转换为整型,在必要时可进行显式类型转换:
enum egg_old {small,medium,large,jumbo};
enum struct t_shirt {small,medium,large,jumbo};
egg_old one = small;
t_shirt rolf = t_shirt::small;
int king = one; //允许
int ring = rolf; //不允许
int ring = (int)rolf; //允许
枚举用某种底层整型类型表示,C++11作用域内枚举的底层类型为int.
stock类非常具体。然而,程序员常常通过定义类来标识更通用的概念,对于实现抽象数据类型(ADT),使用类是一种非常好的方式。ADT以通用的方式描述数据类型,而没有引入语言或实现细节。例如通过栈来储存或删除数据。
栈的操作有 创建空栈 压入 弹出 看栈是否填满 栈是否为空。
可以将上述描述转换为一个类声明,其公有成员函数提供了表示栈操作的接口,私有数据成员负责存储栈数据。类概念非常适合于ADT方法。私有部分必须标明数据存储的方式,可以使用常规数组、动态分配数组或更高级的数据结构。然而,公有接口应隐藏数据表示,而以通用的术语来表达,如创建栈,压入等。
示例:
头文件:
#ifndef STOCK00_H_
#define STOCK00_H_
typedef unsigned long Item;
class Stack
{
private:
enum{MAX=10};
Item items[MAX];
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item& item);
bool pop(Item& item);
};
#endif
私有部分表明,栈是使用数组实现的;公有部分隐藏了这一点 。因此,可以使用动态数组来代替数组,而不会改变类的接口。
接口是冗余的,因为pop()和push()返回有关栈状态的信息,而不是void类型。
这个类不是根据特定的类型来定义栈,而是根据通用的Item类型来描述。如果需要double栈或者结构栈,只需要修改typedef语句。类模板提供更强大的办法(ch14).
成员函数定义:
#include
#include"stock00.h"
Stack::Stack() //创建空栈
{
top = 0;
}
bool Stack::isempty() const
{
return top == 0;
}
bool Stack::isfull() const
{
return top == MAX;
}
bool Stack::push(const Item& item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
else
return false;
}
bool Stack::pop(Item& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else return false;
}
默认构造函数确保所有栈被创建时都为空。
源文件:
#include
#include"stock00.h"
int main()
{
using namespace std;
Stack st;
char ch;
unsigned long po;
cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO,or Q to quit.\n";
while (cin >> ch && toupper(ch) != 'Q')
{
while (cin.get() != '\n')
continue;
if (!isalpha(ch))
{
cout << '\a';
continue;
}
switch (ch)
{
case 'A':
case 'a':cout << "Enter a PO number to add:";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'p':
case 'P':if (st.isempty())
cout << "stack already empty\n";
else
{
st.pop(po);
cout << "PO #" << po << " popped\n";
}
break;
}
cout << "Please enter A to add a purchase order,\n"
<< "p to process a PO,or Q to quit.\n";
}
cout << "Bye!\n";
return 0;
}