过程性编程和面向对象编程

过程性编程:首先考虑要遵循的步骤,然后考虑如何让表示这些数据。
面向对象编程(OOP):首先从用户的角度考虑对象(包括数据和操作),然后考虑接口(用户和程序的桥梁)和数据存储,最后使用新的设计方案创键出程序。

  • 举个栗子

如果你是一位OOP程序员,你收到一个项目–设计一个程序表示股票。那么你作为一个资深OOP程序员,你的脑回路应该是这样的。

  1. 股票的种类那么多,怎么表示股票?我们可以用一个表示股票的类型,每一种股票就是一个对象
  2. 作为一家公司老总,我们对股票有哪些操作?
    • 获得股票
    • 增持
    • 卖出股票
    • 更新股票价格
    • 显示股票信息
  3. 实现这些操作后,股票要具备哪些数据?
    • 公司名称
    • 所持股票数量
    • 每股价格
    • 股票总价
  4. 设计方案

总之,面向对象编程就是从用户的角度来出发,来设计程序。这里的用户可以是人,也可以是程序。

C++中的类

什么是类?类就是一种抽象的类型,就像上面的股票类型
什么是对象?对象就是类中一个具体实例,就像上面的具体的一种股票。

  • 如何定义类?

类一般由两个部分组成:
类声明:类似于函数原型。描述数据成员和函数成员,公有部分和私有部分。
类方法定义:类似于函数定义。具体实现函数成员的定义。

类声明

//类1.h
#ifndef aa
#define aa

#include<iostream>
#include<string>
class Stock
{
private:
std::string company;//公司名
long shares;//所持股票数量
double share_val;//每股价格
double total_val;//股票总价
void set_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

可以看出来这里有几个关键词class,private,public.
class:用来声明类。
private:类的私有部分(private关键词也可以省略)
public:类的公有部分
数据项通常放在私有部分,接口的类成员函数通常放在公有部分

将数据和方法组成一个单元是类最吸引人的特性。不管是公有部分,还是私有部分,成员可以是数据也可以是函数。
数据隐藏是OOP的一种伟大思想,类划分了公有部分和私有部分,对象可以访问公有部分,不能直接访问私有部分,但是可以通过公有成员函数(或者友元函数)来访问私有部分。公有成员函数是用户和对象的私有数据沟通的桥梁,这也是我们所说的公有接口。只有在定义类的时候可以直接访问类的私有部分。

  • 实际上,你如果C语言学的很好,你会发现,类和结构非常相似。
struct Stock
{
std::string company;//公司名
long shares;//所持股票数量
double share_val;//每股价格
double total_val;//股票总价
void (*set_tot)()

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)();//显示信息
}

看看上面这段代码,我们使用结构体和函数指针就可以实现,股票结构体的数据和操作。当我们有一个结构对象a时,我们可以使用a.acqure(....)来获得股票。所以说,将数据和方法组成一个单元并非类的专属。
但是,结构体无法区分公有部分和私有部分,所以说类是加强版的结构体

类方法定义

//类1.cpp
#include<iostream>
#include"类1.h"

void Stock::acquire(const std::string &co,long n,double pr)
{
company=co;
if(n<0)
{
std::cout<<"数量不能是负数"
<<company<<"的股票数量将设置成0\n";
shares=0;
}
else
{
shares=n;
}
share_val=pr;
set_tot();
}

void Stock::buy(long num,double price)
{
if(num<0)
{
std::cout<<"数量不能是负数。交易失败。\n";
}
else
{
shares+=num;
share_val=price;
set_tot();
}
}
void Stock::sell(long num,double price)
{
using std::cout;
if(num<0)
{
cout<<"数量不能是负数.\n";
}
else if(num>shares)
{
cout<<"卖出数量大于持有数量。交易失败。\n";
}
else
{
shares-=num;
share_val=price;
set_tot();
}
}
void Stock::show()
{
using std::cout;
using std::endl;
using std::ios_base;
ios_base::fmtflags orig=cout.setf(ios_base::fixed,ios_base::floatfield);
std::streamsize prec =cout.precision(3);

cout<<"公司:"<<company<<" 股票数量:"<<shares<<endl
<<"每股价格:$"<<share_val;

cout.precision(2);
cout<<" 总价值:$"<<total_val<<endl;

cout.setf(orig,ios_base::floatfield);
cout.precision(prec);

}

类方法定义就是实现类成员函数。成员函数的定义与常规函数的定义非常相似。但是成员函数的定义有3个特征:

  1. 要使用作用域解析符::
  2. 可以直接访问类的私有成员
  3. 在类成员函数中,不必使用作用域解析符,就可以直接使用类成员数据和调用类成员函数。(因为它们同属一个类,所以成员都是可见的)
  • 内联方法

在类声明中,有一个私有成员函数set_tot(),由于它是私有的,只有编写这个类的时候,可以调用它,而用户不能调用它,使用私有成员函数主要是省去一些重复代码的输入。
除了这个特征以外,我们发现,set_tot()函数,在声明的时候直接给出了定义。类声明常将短小的成员函数作为内联函数,所以这个函数实际上会被编译器变成内联函数。

  • 方法使用哪个对象

实际上,每个对象都有它的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法。

使用类

//类1main.cpp
#include<iostream>
#include"类1.h"
int main()
{
Stock x;
x.acquire("USTC",20,12.50);
x.show();
x.buy(15,18.125);
x.show();
x.sell(400,20.00);
x.show();
x.buy(300000,40.125);
x.show();
x.sell(300000,0.125);
x.show();
return 0;
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ .\类1.cpp .\类1main.cpp -o 类1
PS D:\study\c++\path_to_c++> .\类1.exe
公司:USTC 股票数量:20
每股价格:$12.500 总价值:$250.00
公司:USTC 股票数量:35
每股价格:$18.125 总价值:$634.38
卖出数量大于持有数量。交易失败。
公司:USTC 股票数量:35
每股价格:$18.125 总价值:$634.38
公司:USTC 股票数量:300035
每股价格:$40.125 总价值:$12038904.38
公司:USTC 股票数量:35
每股价格:$0.125 总价值:$4.38

构造函数

我们发现我们无法像结构体那样Stock hot={"Cam",100,50.25};这样初始化对象,这是因为类的数据成员都是私有的,所以我们无法直接访问数据成员。当然了,如果Stock类的数据成员都是公有的,我们自然能像结构体那样直接初始化对象。
实际上,我们发现我们Stock::acquire()函数,就是扮演着初始化的对象的角色。
C++给类提供了一个构造函数,专门用来创造和初始化对象的。

声明和定义构造函数

Stock(const std::string &co,long n=0,double pr=0.0);//构造函数

上面这个就是构造函数的声明,这里使用了默认参数,我们发现构造函数的声明和成员函数的声明很像,但是注意,没有返回类型、构造函数的名称和类名称相同、构造函数是公有部分中的

Stock::Stock(const std::string &co,long n,double pr)
{
company=co;
if(n<0)
{
std::cout<<"数量不能是负数"
<<company<<"的股票数量将设置成0\n";
shares=0;
}
else
{
shares=n;
}
share_val=pr;
set_tot();
}

上面这个就是构造函数的定义,这段代码和acquire()的代码是一致的。

使用构造函数

C++提供了两种调用构造函数的方式。

  1. 显式调用
    Stock food = Stock("World",250,1.25);
    有没有发现啊?实际上这里利用了重载的技术,前面那个Stock是一个类,后面那个Stock是一个构造函数,而且我们发现构造函数的返回类型其实就是Stock类。
  2. 隐式调用
    Stock food("World",250,1.25);

我们也可以使用动态内存分配的方式创建类对象
Stock *pfood =new Stock("World",250,1.25);
但是有一点请注意:
构造函数不同于一般的成员函数,它不能使用诸如food.Stock("World",250,1.25)的方式调用构造函数。

默认构造函数

默认构造函数和非默认构造函数的差别巨大,而且默认构造函数和非默认构造函数一样重要

  • 什么是默认构造函数?

    上一节所说的构造函数都是非默认构造函数,非默认构造函数的功能时提供对象初始化。
    默认构造函数的功能是,当对象未初始化时,给它进行默认初始化。

  • 为什么要有默认构造函数?

    C++禁止创建未初始化的对象。如果你只提供了非默认构造函数,那么Stock food;语句会直接报错,因为对象未初始化。但是如果你不提供非默认构造函数,那么Stock food;将不会报错,因为C++会提供隐式默认构造函数,自动给对象初始化。

  • 有两种默认构造函数

    隐式默认构造函数
    如果没有提供任何构造函数,C++将自动提供隐式默认构造函数,它的定义可能是
    Stock::Stock(){}
    隐式默认构造函数不做任何操作,它只是告诉编译器,“你看,对象初始化过了!”
    显式默认构造函数
    我们可以自己定义一个默认构造函数,这个就叫做显式默认构造函数
    一种简单的方法是,直接给非默认构造函数提供所有的默认参数,
    Stock(const std::string &co="Error!",long n=0,double pr=0.0);
    另一种方法是,利用构造函数的重载,显式默认构造函数原型:
    Stock();可以发现默认构造函数不接受任何参数。
    显式默认构造函数定义:
    Stock::Stock(){company="no name";shares=0;share_val=0.0;total_val=0.0;}
    当然了,为了重载解析成功,你只能二选一,两种方法仁者见仁,智者见智慧。

一般来说,类声明中,应该同时提供非默认构造函数和显式默认构造函数
这节所说的一切,本质上是函数重载,因为非默认构造函数和显式默认构造函数函数名相同,你调用哪个取决于语法。

Stock a;
Stock b("world",250,2.50);

上述代码中,对象a采用的是显式默认构造函数,对象b采用的是非默认构造函数。

析构函数

析构函数完成清理工作,因此非常有用。
如果构造函数中使用new来创建数据,那么你必须写一个析构函数,用来delete各种数据项。当然了,如果你的构造函数中不使用动态内存分配,那么你完全不需要写析构函数。但是就算你不写析构函数,编译器也会自动给你生成一个什么也不干的隐式的默认析构函数。

  • 析构函数什么时候被调用?

    如果是静态对象,程序结束时,自动调用。
    如果是自动对象,代码段结束时,自动调用。
    如果是用new创建的对象,使用delete时自动调用。
    总之,析构函数是自动调用的

  • 析构函数的原型和实现
    ~Stock();可以看出来析构函数没有返回类型,而且不接受任何参数,析构函数的函数名是类名前加一个~

    Stock::~Stock(){
    cout<<"Bye!"<<company<<endl;
    }

改进Stock

//类2.h
#ifndef aa
#define aa

#include<iostream>
#include<string>
class Stock
{
private:
std::string company;//公司名
long shares;//所持股票数量
double share_val;//每股价格
double total_val;//股票总价
void set_tot()
{
total_val=shares*share_val;
}
public:
Stock();//默认构造函数
Stock(const std::string &co,long n=0,double pr=0.0);//构造函数
~Stock();//析构函数
void buy(long num,double price);//增持
void sell(long num,double price);//卖出股票
void update(double price);//更新股票价格
void show();//显示信息

};
#endif
//类2.cpp
#include<iostream>
#include"类2.h"
Stock::Stock()
{
company="no name";
shares=0;
share_val=0.0;
total_val=0.0;
std::cout<<"注意!默认初始化!\n";
}
Stock::Stock(const std::string &co,long n,double pr)
{ std::cout<<"注意!使用构造函数初始化,"<<co<<std::endl;
company=co;
if(n<0)
{
std::cout<<"数量不能是负数"
<<company<<"的股票数量将设置成0\n";
shares=0;
}
else
{
shares=n;
}
share_val=pr;
set_tot();
}
Stock::~Stock()
{
std::cout<<"注意!再见,"<<company<<std::endl;
}
void Stock::buy(long num,double price)
{
if(num<0)
{
std::cout<<"数量不能是负数。交易失败。\n";
}
else
{
shares+=num;
share_val=price;
set_tot();
}
}
void Stock::sell(long num,double price)
{
using std::cout;
if(num<0)
{
cout<<"数量不能是负数.\n";
}
else if(num>shares)
{
cout<<"卖出数量大于持有数量。交易失败。\n";
}
else
{
shares-=num;
share_val=price;
set_tot();
}
}
void Stock::update(double price){
share_val=price;
set_tot();
}
void Stock::show()
{
using std::cout;
using std::endl;
using std::ios_base;
ios_base::fmtflags orig=cout.setf(ios_base::fixed,ios_base::floatfield);
std::streamsize prec =cout.precision(3);

cout<<"公司:"<<company<<" 股票数量:"<<shares<<endl
<<"每股价格:$"<<share_val;

cout.precision(2);
cout<<" 总价值:$"<<total_val<<endl;

cout.setf(orig,ios_base::floatfield);
cout.precision(prec);

}

//类2main.cpp
#include<iostream>
#include"类2.h"
int main()
{
{
using std::cout;
using std::endl;
cout<<"使用构造函数创建两个对象"<<endl;
Stock stock1("公司1",12,20.0);
stock1.show();
Stock stock2=Stock("公司2",2,2.0);
stock2.show();

cout<<"把stock1赋值给stock2"<<endl;
stock2=stock1;
cout<<"显示stock1和stock2"<<endl;
stock1.show();
stock2.show();

cout<<"使用构造函数给对象重新初始化"<<endl;
stock1=Stock("公司3",10,50.0);
cout<<"重新显示stock1"<<endl;
stock1.show();
cout<<"完成!"<<endl;
}
std::cout<<"代码段结束\n";
return 0;
}
PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 类2 .\类2.cpp .\类2main.cpp
PS D:\study\c++\path_to_c++> .\类2.exe
使用构造函数创建两个对象
注意!使用构造函数初始化,公司1
公司:公司1 股票数量:12
每股价格:$20.000 总价值:$240.00
注意!使用构造函数初始化,公司2
公司:公司2 股票数量:2
每股价格:$2.000 总价值:$4.00
把stock1赋值给stock2
显示stock1和stock2
公司:公司1 股票数量:12
每股价格:$20.000 总价值:$240.00
公司:公司1 股票数量:12
每股价格:$20.000 总价值:$240.00
使用构造函数给对象重新初始化
注意!使用构造函数初始化,公司3
注意!再见,公司3
重新显示stock1
公司:公司3 股票数量:10
每股价格:$50.000 总价值:$500.00
完成!
注意!再见,公司1
注意!再见,公司3
代码段结束

可以试着跑一下上面的程序,可能结果有不同,因为不同编译器对Stock stock2=Stock("公司2",2,2.0);的处理不同,这句话可能创建临时对象。
stock1=Stock("公司3",10,50.0);这句是赋值语句是一定会创建临时对象的。
所以说,通常来说,初始化比赋值更高效

下面看看一些有趣的对象初始化:

//类3.cpp
#include"类2.h"
#include"类2.cpp"

int main(){
Stock a={"腾讯",100,45.0};
a.show();
Stock b{};
b.show();
const Stock c{"网易",200,25.00};
//c.show();

}
注意!使用构造函数初始化,腾讯
公司:腾讯 股票数量:100
每股价格:$45.000 总价值:$4500.00
注意!默认初始化!
公司:no name 股票数量:0
每股价格:$0.000 总价值:$0.00
注意!使用构造函数初始化,网易
注意!再见,网易
注意!再见,no name
注意!再见,腾讯

可以看出来,列表初始化语法同样适用于类
但是我这边注释掉了一段代码//c.show();,这段代码运行会出错。
因为,c是一个const对象,c.show()无法确保c的数据成员不被修改,所以编译器会报错。
那么,我们想到的方法肯定是–const形参,但是,正如你所见,show()函数,没有形参,该怎么办?
const成员函数
C++允许我们这样声明、定义const成员函数:
void show() const;

void Stock::show() const
{
...
}

实际上,C++的初始化语句非常多,源于运算符的重载。
下面看一个神奇的例子。
如果一个类的构造函数是Bozo(int a);,它的构造函数只接受一个参数,那么初始化语句可以这样写Bozo x=6;
接受一个参数的构造函数允许使用赋值语法将对象初始化:
classname object = value;
这种语法有危险!。