C++ Primer Plus读书笔记
类与对象
1.面向对象编程和过程性编程
- 编程模型
所有计算机均由两种元素组成:代码和数据.精确的说,有些程序是围绕着”什么正在发生”而编写,有些则是围绕”谁正在受影响”而编写的.
- 第一种编程方式叫做”面向过程的模型”,按这种模型编写的程序以一系列的线性步骤(代码)为特征,可被理解为作用于数据的代码.如 C 等过程化语言.
- 第二种编程方式叫做”面向对象的模型”,按这种模型编写的程序围绕着程序的数据(对象)和针对该对象而严格定义的接口来组织程序,它的特点是数据控制代码的访问.通过把控制权转移到数据上,面向对象的模型在组织方式上有:抽象,封装,继承和多态的好处.
抽象
面向对象程序设计的基本要素是抽象,可以根据物理含义分解一个复杂的系统,把它划分成更容易管理的块.
例如,一个计算机系统是一个独立的对象.而在计算机系统内部由几个子系统组成:显示器,键盘,硬盘驱动器,DVD-ROM,软盘,音响等,这些子系统每个又由专门的部件组成.关键是需要使用层次抽象来管理计算机系统(或其他任何复杂系统)的复杂性.
封装
封装是一种把代码和代码所操作的数据捆绑在一起,使这两者不受外界干扰和误用的机制.封装可被理解为一种用做保护的包装器,以防止代码和数据被包装器外部所定义的其他代码任意访问.对包装器内部代码与数据的访问通过一个明确定义的接口来控制.
封装代码的好处是每个人都知道怎样访问代码,进而无需考虑实现细节就能直接使用它,同时不用担心不可预料的副作用.
- 一个类定义着将由一组对象所共享的行为(数据和代码).一个类的每个对象均包含它所定义的结构与行为,这些对象就好象是一个模子铸造出来的.所以对象也叫做类的实例.
- 在定义一个类时,需要指定构成该类的代码与数据.特别是,类所定义的对象叫做成员变量或实例变量.操作数据的代码叫做成员方法.方法定义怎样使用成员变量,这意味着类的行为和接口要由操作实例数据的方法来定义.
- 类的公有接口代表外部的用户应该知道或可以知道的每件东西.
- 私有的方法数据只能通过该类的成员代码来访问.这就可以确保不会发生不希望的事情.
继承
继承是指一个对象从另一个对象中获得属性的过程.是面向对象程序设计的三大原则之二,它支持按层次分类的概念.例如,波斯猫是猫的一种,猫又是哺乳动物的一种,哺乳动物又是动物的一种.如果不使用层次的概念,每个对象需要明确定义各自的全部特征.通过层次分类方式,一个对象只需要在它的类中定义是它成为唯一的各个属性,然后从父类中继承它的通用属性.
因此,正是由于继承机制,才使得一个对象可以成为一个通用类的一个特定实例.一个深度继承的子类将继承它在类层次中的每个祖先的所有属性. 继承与封装可以互相作用.
- 如果一个给定的类封装了某些属性,它的任何子类将会含有同样得属性,另加各个子类所有得属性.这是面向对象程序在复杂性上呈线性而非几何增长的一个重要概念.新的子类继承其所有祖先的所有属性.子类和系统中的其他代码不会产生无法预料的交互作用.
多态
多态是指一个方法只能有一个名称,但可以有许多形态,也就是程序中可以定义多个同名的方法,用”一个接口,多个方法”来描述.可以通过方法的参数和类型引用.使用类的好处在于能够将代码的控制交给数据,将数据与程序合为一体,管理起来更加整齐,而且相对于面向过程的编程来说更加简洁,同时其继承又使得编程变得更加方便。类就相当于结构的一个加强版,总的来说,结构也能实现继承、多态,能包含成员函数。 唯一的区别就是二者的默认数据访问控制类型不同。
2. 类
类的访问控制:关键字private和public用来描述对类成员的访问控制,使用类对象的程序都可以直接访问public部分,但是只有成员函数和友元函数才能访问private部分,将数据隐藏(使用private)也是数据封装的一种形式。
类与结构(class与struct)的唯一区别在于,结构的默认访问类型是public,而类的默认访问类型则是private。
类的内联方式:定义位于类的声明中的函数都自动成为内联函数
使用类之前需要创建类对象,可以用new为对象分配存储空间,可以将对象作为函数的返回值和参数,相似的类的对象之间可以转换
对于一个类来说,
sizeof()
的结果是类内所有元素的大小,类内的函数都不计算在内。
3.类的构造与析构
使用构造与析构函数的原因:在初始化对象的时候由于某些数据是private不能够直接访问的,因此需要借助类中的函数进行初始化,因此就有了构造函数和析构函数这一说。
构造函数是一个没有返回类型的函数。
- 一般来说,要留一个空参数的默认构造函数,主要为了防止没有显式制定
//头文件
#ifndef ZHUMENG_H_
#define ZHUMENG_H
#include<iostream>
class ZhuMeng
{
private:
float yanyiquan;
public:
ZhuMeng(float a);
~ZhuMeng();
void Show();
};
#endif
//实现文件
#include"ZhuMeng.h"
ZhuMeng::ZhuMeng(float a)
{
yanyiquan = a;
}
ZhuMeng::~ZhuMeng()
{
}
void ZhuMeng::Show()
{
std::cout << yanyiquan << std::endl;
}
//调用文件
#include<iostream>
#include"ZhuMeng.h"
using namespace std;
int main()
{
ZhuMeng quanquan(3.14);
ZhuMeng chunjie(4.16);
quanquan.Show();
chunjie.Show();
return 0;
}
- 析构函数:如果在构造函数中使用new分配内存,则析构函数将使用delete来释放这些内存,但如果没有使用new分配内存的话,则不需要在析构函数中做这些事情。
4.this指针
每个对象都拥有一个指针:this指针,通过this指针来访问自己的地址。
注:this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。
- this在成员函数的开始执行前构造的,在成员的执行结束后清除。
- this 就是这个类(层级最近的)自己(做为指针而非引用),-> 就是调用类指针的某个成员变量或者函数。当你在类的实现里使用类的各种方法、成员变量的时候,如果不加上 this-> 编译器会自动的给你加上。有一些时候,你不得不显示的加上,因为函数参数名如果和成员变量重名,编译器会优先选层级近的,这时候如果要用成员变量,需要显式的加上 this-> 。
- 等于说一般情况下this是没有什么作用的,只是在特定的情况下才必须使用this指针。
5.const
- 函数的前面和后面加const效果是不太相同的,
- 在函数前加const表示返回型为const型;
- 在函数后面加const表示函数不能修改类内数据成员;
- 在函数的参数前加const是另一种写法,表示这个内容在函数执行期间不会被更改。
//在类内声明为:
int func(int ) const;
//表示为const成员函数,在任何情况下该函数都不会修改数据成员。
6.运算符重载
在类里面重载运算符,主要是为了使得运算符适应于这个类。
注意:运算符重载的函数只能带一个参数,假设重载一个加法,this指针指向的是运算符左边的对象,同时,运算符右边的数据类型不限,但是需要在定义重载运算函数的参数列表中确定。
其实应该这样看,比如重载了一个运算符+,这时候
C=A+B 被解释为
C=A.operator+(B)
//头文件
#ifndef ZHUMENG_H_
#define ZHUMENG_H
#include<iostream>
class ZhuMeng
{
private:
int shi;
int fen;
int miao;
public:
ZhuMeng();
ZhuMeng(int );
ZhuMeng(int , int , int );
~ZhuMeng();
ZhuMeng operator+(ZhuMeng &) const;
};
#endif
//实现文件
#include"ZhuMeng.h"
ZhuMeng::ZhuMeng()
{
shi = 0;
fen = 0;
miao = 0;
}
ZhuMeng::ZhuMeng(int shit)
{
this->shi = shit;
}
ZhuMeng::ZhuMeng(int shi, int fen, int miao)
{
this->shi = shi;
this->fen = fen;
this->miao = miao;
}
operator double()
{
return this->miao;
}
ZhuMeng::~ZhuMeng()
{
}
ZhuMeng ZhuMeng::operator+(ZhuMeng & aaa) const
{
std::cout << this->shi << std::endl;
ZhuMeng sum;
sum.shi = this->shi + aaa.shi;
sum.fen = this->fen + aaa.fen;
sum.miao = this->miao + aaa.miao;
return sum;
}
//调用
#include<iostream>
#include"ZhuMeng.h"
using namespace std;
int main()
{
ZhuMeng default_zhumeng;
ZhuMeng a1(13, 24, 10);
ZhuMeng a2(3, 3, 3);
default_zhumeng = a1 + a2;
return 0;
}
由于某些运算符的左右类型不同,比如一个类+一个int型,这样如果把int写在加号的左边就会出现问题
- A+2.4
- 2.4+A
- 上面两种描述是不同的,这时候使用非成员函数进行运算符重载会比较合适
非成员函数的运算符重载是不写在类内的,在类外进行定义的时候重载运算符的参数有两个。下面是一个实例:
ZhuMeng operator*(int all, ZhuMeng all_2)
{
cout<<"success"<<endl;
}
int main()
{
ZhuMeng default_zhumeng;
ZhuMeng a1(13, 24, 10);
ZhuMeng a2(3, 3, 3);
default_zhumeng = 3*a1;
return 0;
}
重载的限制:
以下运算符不可以重载
sizeof
.
成员运算符.*
成员指针运算符::
作用域解析运算符?:
条件运算符typeid
RTTI运算符const_cast
强制类型转换dynamic_cast
强制类型转换reinterpret_cast
强制类型转换static_cast
强制类型转换
不能创建新的运算符,例如不能使用operator**()函数来创建运算符
7.友元
定义:使用非成员函数可以按所需的顺序获得操作数,但是这样的话会引入一个新的问题,非成员函数不能直接访问private,这时候就引出了友元函数的概念,友元函数(非成员函数)是可以访问类的私有成员的。
友元有三大类
- 友元函数
- 友元类
- 友元成员函数
创建友元
第一步:把其原型放在类声明中,并在原型声明前加上关键字
friend
。例如friend ZhuMeng operator+(int a, ZhuMeng b);
上面的原型意味着
- 虽然operator+()函数是在类声明中声明的,但是它不是成员函数,不能使用成员运算符来调用。
- 虽然operator+()函数不是成员函数,但它与成员函数的访问权限相同。
- 第二步:编写函数定义
- 因为它不是成员函数,因此不需要使用ZhuMeng::限定符。另外,
在定义的时候不需要带上friend关键字
- 因为它不是成员函数,因此不需要使用ZhuMeng::限定符。另外,
8.类的自动转换和类型转换
类的构造函数中定义了某种方法,则可以直接用构造函数的参数列表给一个对象逐成员赋值,例如下面两行代码就是。
下面代码的第四行,也就类似于默认的强制类型转换,将一个整数类型强制转换成了类的对象。
ZhuMeng default_zhumeng;
int k = 3;
default_zhumeng = 3,3,3;//隐式强制类型转换
default_zhumeng = ZhuMeng(k);//显式强制类型转换
如果在构造函数的声明中使用了关键字
explicit
,则只能通过显式进行强制类型转换而不能通过隐式强制类型转换。转换函数:将一个特定类的对象转换成默认的类型,转换函数是没有返回值类型的,也没有参数列表,这里转换函数的形式为:
operator typeName()
如果定义了多个类型的不同转换函数,则不能使用隐式的自动转换,因为可能会产生二义性,编译器不知道使用哪一种转换函数,因此需要使用显式的转换方式进行转换。
转换函数与友元函数
- 比如要实现一个特定类的加法,有一种可行的策略是使用类型转换类进行,也可以设置成员函数或非成员函数进行操作,都是可行的。