过程性编程和面向对象编程
过程性编程:首先考虑要遵循的步骤,然后考虑如何让表示这些数据。
面向对象编程(OOP):首先从用户的角度考虑对象(包括数据和操作),然后考虑接口(用户和程序的桥梁)和数据存储,最后使用新的设计方案创键出程序。
如果你是一位OOP程序员,你收到一个项目–设计一个程序表示股票。那么你作为一个资深OOP程序员,你的脑回路应该是这样的。
- 股票的种类那么多,怎么表示股票?我们可以用一个类表示股票的类型,每一种股票就是一个对象。
- 作为一家公司老总,我们对股票有哪些操作?
- 获得股票
- 增持
- 卖出股票
- 更新股票价格
- 显示股票信息
- 实现这些操作后,股票要具备哪些数据?
- 设计方案
总之,面向对象编程就是从用户的角度来出发,来设计程序。这里的用户可以是人,也可以是程序。
C++中的类
什么是类?类就是一种抽象的类型,就像上面的股票类型
什么是对象?对象就是类中一个具体实例,就像上面的具体的一种股票。
类一般由两个部分组成:
类声明:类似于函数原型。描述数据成员和函数成员,公有部分和私有部分。
类方法定义:类似于函数定义。具体实现函数成员的定义。
类声明
#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(....)
来获得股票。所以说,将数据和方法组成一个单元并非类的专属。
但是,结构体无法区分公有部分和私有部分,所以说类是加强版的结构体
类方法定义
#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个特征:
- 要使用作用域解析符
::
- 可以直接访问类的私有成员
- 在类成员函数中,不必使用作用域解析符,就可以直接使用类成员数据和调用类成员函数。(因为它们同属一个类,所以成员都是可见的)
在类声明中,有一个私有成员函数set_tot()
,由于它是私有的,只有编写这个类的时候,可以调用它,而用户不能调用它,使用私有成员函数主要是省去一些重复代码的输入。
除了这个特征以外,我们发现,set_tot()
函数,在声明的时候直接给出了定义。类声明常将短小的成员函数作为内联函数,所以这个函数实际上会被编译器变成内联函数。
实际上,每个对象都有它的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法。
使用类
#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++提供了两种调用构造函数的方式。
- 显式调用
Stock food = Stock("World",250,1.25);
有没有发现啊?实际上这里利用了重载的技术,前面那个Stock
是一个类,后面那个Stock
是一个构造函数,而且我们发现构造函数的返回类型其实就是Stock
类。
- 隐式调用
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
各种数据项。当然了,如果你的构造函数中不使用动态内存分配,那么你完全不需要写析构函数。但是就算你不写析构函数,编译器也会自动给你生成一个什么也不干的隐式的默认析构函数。
改进Stock
类
#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
|
#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);
}
|
#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);
这句是赋值语句是一定会创建临时对象的。
所以说,通常来说,初始化比赋值更高效。
下面看看一些有趣的对象初始化:
#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};
}
|
注意!使用构造函数初始化,腾讯 公司:腾讯 股票数量: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;
这种语法有危险!。