10.1 过程性编程和面向对象编程
面向对象特性
面向对象编程 object-oriented programming
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码可重用
封装
将实现细节放在一起并将它们与抽象分开称为封装。数据隐藏是把数据放在类的私有部分中,数据隐藏是一种封装,将类函数定义和类声明放在不同的文件中也是一种封装。
10.2 抽象与类
类
类首字母一般大写
类规范由两个部分组成
- 类声明:提供蓝图:class里面的
- 按数据和函数成员分:以数据成员的方式描述数据部分,以成员函数(称为方法)的方式描述公有接口
- 按共有私有分:私有部分只能通过成员函数访问,公有部分的成员能被使用类对象的程序直接访问。
- 通常数据成员放在私有部分,成员函数放在公有部分
- 类方法定义:提供细节:描述如何实现类成员函数
通常接口(类定义/类声明)放在头文件中,实现(类方法的代码)放在源代码文件中。
类声明模板:
class className
{
private:
data member declarations;
public:
member function prototypes;//成员函数原型
};
成员函数原型是不写出实现方式,只写传入传出,原地定义则是写出实现方式,比如void buy(long num,double price);
就是函数原型
公有类访问和私有类访问
private和public,private是类对象的默认访问控制
类成员函数
::
为作用域运算符解析
特征1:定义成员函数时,使用作用域解析运算符::来标识函数所属的类。
Stock::update()为函数的限定名,update()是全名的缩写,非限定名,只能在类作用域中使用
特征2:类方法可以访问类的private组件
class Stock
{
private:
public:
void buy(int num,double price);
}
void Stock::buy(long num,double price)
{
//可以直接使用类里面的数据成员
}
内联函数
定义位于类声明中的函数自动成为内联函数,也可以在类声明之外定义成员函数,使其成为内联函数,使用inline
class Stock
{
private:
//法1:类声明中定义方法
void set_tot() { total_val=shares * share_val; }
//法2:原型替换方法定义
void set_tot2();
}
//类声明后改写定义为内联函数,法1法2等价
inline void Stock::set_tot()
{
······
}
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。为了确保内联定义对多文件程序中的所有文件都可用,可以把内联定义放在头文件。
const成员函数
任何不会修改数据成员的函数都应该声明为const类型。
const成员函数不可以修改对象的数据,不管对象是否具有const性质。它在编译时,以是否修改成员数据为依据进行检查,如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误。
然而加上mutable修饰符的数据成员,任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的。
例:
const Stock & topval(const Stock & s) const
该函数隐式地访问一个对象,而显式地访问另一个对象,并返回其中一个对象的引用。
stock1.topval(stock2);
隐式地访问stock1,显式地访问stock2
括号中的const:该函数不会修改被显式地访问的对象
括号后的const:该函数不会修改被隐式地访问的对象
最开始的const:由于函数返回了两个const对象之一的引用,因此返回类型也应该为const引用
对象
创建对象
声明类变量 ClassA A,B;
OOP中,调用成员函数被称为发送消息
用函数返回对象
const Stock & topval(const Stock & s) const
该函数隐式地访问一个对象,而显式地访问另一个对象,并返回其中一个对象的引用。
10.3 类的构造函数和析构函数
构造函数
为了解决初始化时数据成员不能直接访问的问题,需要在创建对象时自动对其初始化。
类构造函数专门用于构造新对象,将值赋给它们的数据成员。
特征
-
构造函数没有声明类型,没有返回值,默认构造函数可能没参数,非默认构造函数一般有参数。
-
程序在声明对象时,会自动调用构造函数。
-
构造函数用来创建对象,而不能通过对象来调用
注意构造函数的参数表示的不是类成员,而是赋给类成员的值,因此参数名不能与类成员相同。
调用构造函数
显式调用:Stock food=Stock("World cabbage",250,1.25);
隐式调用:Stock food("World cabbage",250,1.25);
与new一起使用:Stock *pstock=new Stock("Electro",18,19.0);
这种情况下对象没有名称,但是可以使用指针来管理对象
初始化列表
https://blog.youkuaiyun.com/weixin_45452278/article/details/124869215
初始化列表是一种便捷的初始化类内成员变量的方式,
初始化成员变量通常在构造函数中执行,可以通过
- 调用有参的构造函数进行传参初始化
- 调用无参的构造函数在函数体内部直接初始化
成员变量的初始化列表只能用在构造函数的后面,不能用在其他函数后面
class Person{
int m_age;
int m_height;
public:
Person(int age,int height){//构造函数的形参
m_age = age;
m_height = height;
}
};
void main(){
Person person(10,60);
cout << person.m_age << endl; //10
cout << person.m_height << endl; //60
}
Person(int age, int height) : m_age(age), m_height(height) {}
两个person函数等价,效率是一样的
初始化列表的优势:
- 传入参数可以是表达式
- 传入参数可以是函数
初始化成员变量的顺序,只跟成员变量在内存中的地址值有关,即只和类中声明成员变量的顺序有关。
需要做到成员变量的定义顺序和初始化列表顺序一致,增强可读性。
初始化列表和默认参数搭配
默认参数作用:声明对象时,不传递参数也有默认参数传递参数。
初始化列表与默认参数搭配使用最大的好处就是:我写一个构造函数,相当于写了三个构造函数。
class Person
{
int m_age;
int m_height;
public:
Person(int age = 10 , int height = 50) : m_age(age),m_height(height){ }
}
默认构造函数
特征
- 如果没有提供任何构造函数,则C++自动隐式提供默认构造函数,该函数不做任何工作;但如果定义了构造函数,就必须显式提供默认构造函数,不然会报错。
- 默认构造函数没有参数,因为声明中不包含值
- 隐式的调用默认构造函数时,不要使用圆括号
- 可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同
定义默认构造函数
一种方法是给已有的构造函数的所有参数提供默认值
Stock(const string & co = "Error",int n=0,double pr=0.0)
另一种是通过函数重载来定义另一个没有参数的构造函数
Stock();
但是不能同时采用这两种方式
//1.隐式调用默认构造函数
Stock first;
//2.显式调用默认构造函数
Stock first = Stock();
//3.隐式调用默认构造函数
Stock *p=new Stock;
//4.调用非默认构造函数,即接受参数的构造函数
Stock first("concrete con");
//5.声明一个函数,其返回值为Stock对象
Stock second();
//6.显式调用非默认构造函数
Stock *pstock=new Stock("Electro",18,19.0);
默认构造函数可以显式调用(不加括号),也可以隐式调用(加括号),但非默认构造函数只能显式调用
Bozo(const char * fname, const char * lname);
//初始化
Bozo b1=Bozo("fname","lname");
Bozo b2("fname","lname");
Bozo *pb3=new Bozo("fname","lname");//动态对象
//C11初始化
Bozo c1={"fname","lname"};
Bozo c2{"fname","lname"};
Bozo *pc3=new Bozo{"fname","lname"};//动态对象
总之三种方法:
- 不加等号,不用new
- 不用new,加等号
- 用new且加等号
单参数构造函数
如果构造函数原型为Bozo(int age)
,则可以用Bozo tubby =32
来初始化对象。
因为接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值。这个特性是可以被关闭的。
析构函数
特征
析构函数完成清理工作,如果构造函数用new分配内存,那么析构函数就用delete释放内存
- 析构函数没有参数,没有返回值和声明类型。
- 名称为类名称前加上
~
- 如果构造函数使用了new,则必须提供使用delete的析构函数
析构函数原型:~Stock();
调用
编译器决定什么时候调用析构函数,一般不显式调用
- 静态储存类对象:程序结束时自动调用
- 自动存储类对象:程序执行完代码块时自动调用
- 用new创建的对象:对象驻留在栈内存或自由存储区中,当使用delete释放内存时,析构函数将自动被调用
- 程序创建的临时对象:结束对该对象的使用时自动调用
10.4 this指针
如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它,如果需要显式引用调用它的对象,则可以使用this指针。
定义
this是个特殊指针,this指针指向用来调用成员函数的对象
所有的类方法都将this指针设置为调用它的对象的地址(这里的“它”指被调用的类方法)
this是对象的地址,不是对象本身,*this
可以作为调用对象的别名
const Stock & Stock::topval(const Stock & s) const
{
if(s.total_val>total_val)
return s;
else
return *this;
}//返回类型为引用意味着返回的是调用对象本身,而不是其副本
10.5 对象数组
例如Stock mystuff[4];
见p300
10.6 类作用域
类中定义的名称,如数据成员名和成员函数名的作用域都是整个类(包括类声明和类方法定义)。
类作用域中定义常量
class A
{
private:
const in Months=12;
double costs[Months];
}
以上是行不通的,因为类声明没有创建对象,没有用于存储值的空间
有两种方法可以实现在类中定义常量
枚举
类中声明枚举的作用域为整个类,这里是为了创建符号常量而不是为了创建枚举类型的变量,所以不用提供枚举名。
class A
{
private:
enum {Months=12};
double costs[Months];
}
这种声明枚举并不会创建类数据成员,而是在作用域为整个类的代码中用12替换Months这个符号
static
使用静态变量,可以使Months与其他静态变量一起存储,而非存储在对象中,因此只有一个Months被所有A对象共享。C++98中只能声明整数或者枚举的静态变量,而不能声明double,C++11消除了这种限制。
class A
{
private:
static const int Months=12;
double costs[Months];
}
类中枚举的使用
见p303 10.6.2
10.7 抽象数据类型
类概念非常适合于ADT方法,公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。
比如力扣上的实现某种数据结构的类。