c++面向对象程序设计(上)

c++面向对象程序设计(上)

1.c++编程简介

前提知识

  • 曾经学过某种procedural language
  • 知道一个程序需要编译、连结才能够被执行
  • 知道如何编译和连结(如何建立一个可运行程序)

学习目标

基于对象(Object Based)

  • 培养正规的、大气的编程习惯
  • 以良好的方式编写c++ class
  • class without pointer members - Complex
  • class without pointer members - String

面向对象

  • 学习Classes之间的关系
    • 继承 (inheritance)
    • 复合 (composition)

c++ = 语言 + 标准库

2.头文件与类的声明

c vs c++,关于数据和函数


数据的处理
  • 对于c语言,数据由变量(variables)接收,可以由所有的函数处理
  • 对于c++语言,数据与函数做捆绑,在处理数据时创建对象(object),并使用对象中所包含的函数来对数据进行处理

c vs c++,关于数据与函数

complex类(不含指针)与string类(内含指针)的区别

complex类与string类的区别

Object Based(基于对象)vs Object Oriented(面向对象)


  • Object Based: 面对的是单一的class的设计
  • Object Oriented:面对的是多重classes的设计,classes和classes之间的关系。

c++ programs代码基本形式


c++ programs代码的基本形式

注意:延伸文件名(extension file name)不一定是.h或者.cpp,也可能是.hpp或其他货甚至无延伸名。

Header(头文件)中的防卫式声明


complex.h

#ifndef __COMPLEX__  //如果未曾定义则定义
#define __COMPLEX__

...

#endif

Header(头文件)的布局


  • 前置声明

  • 类-声明

  • 类-定义

// 前置声明
class ostream;
class complex;

// 类-声明
class complex
{
...
};

//类-定义
complex::function...

class template(模版)简介


希望类中某个属性不直接绑定内置数据类型,因此直接把类型抽象成一个模板。

template<typename T>
class complex
{
public:
   complex(T r=0,T i=0):re(r),im(i)//初始化的一种写法
  {}

private:
T re,im;
   
}
---
{
//实例化对象时,指定数据类型
  complex<double> c1(2.5,1.5);
  complex<int>  c2(2,6);
  ...
}

inline(内联)函数


如果函数在class内定义完成,则会成为inline函数(但要求函数简单,编译器不拒绝)。

但某些复杂函数无法inline,编译器会拒绝。

access level(访问级别)


public:可以被class外所调用(一般用于函数);

private:只能够被class类内使用(一般用于数据)(建议);

意义:避免初始化类对象时创建的数据被外部任意修改。

注意: 对于内容足够多的类,不同访问权限的模块可以分段书写多份(不必只使用连续的两段表示)。

3.构造函数

构造函数的通常写法


构造函数的函数名称与类一致。无返回值。

另:不带指针的类通常不需要写析构函数。

//任何函数都可以写默认实参
//使用初值列,initialization list(只有构造函数才有的语法)
complex(double r = 0,double i = 0):
	re(r), im(i)
    {	}
	
//这种赋值方法没有上面好
//原因是:初始化和赋值分离了,多走了一步,而上面一致
complex(double r = 0,double i = 0)
	{ re = r ; im = i; }

ctor(构造函数)可以有很多个-overloading(重载)


  • 同名函数(在编译器看来是不同名函数)可以同时存在,重载不仅指入参可以不一样,出参也可以不一样。

  • 函数重载经常发生在构造函数上

  • 如果同一函数名的两个函数产生歧义(两者意义相同,编译器不知道选择哪个),则无法重载

// 两个重载函数的入参和出参均不同
//get
double real() const{return re;}

//set
void real(double r){re = r;}
//等价构造,不可
complex(double r = 0,double i = 0):
	re(r), im(i){	}

complex():
	re(0), im(0){	}

ctors放在private区


Singleton单例模式

把构造函数放在private中,就不能被类外部公共所创造,只可通过函数调用类内创造的一份对象。

class A{
public:
	static A& getInstance();//通过public权限下的函数调用对象
	setup(){...}
private:
	A(); //默认构造
	A(const A& rhs); //赋值copy构造
}

A& A::getInstance(){        //在类中创建自身,并返回
	static A a;              
	return a;
}

4.参数传递与返回值

const member function(常量成员函数)


类中的函数分为两类,不会改变类内数据会改变类内数据,而其中不会改变类内数据的函数要再函数名后加 const

double real() const {return re};
double imag() const {return im};

不然,可能会发生错误

	//正确
{	
	complex c1(2,1);
	cout<< c1.real();
 	cout<< c2.imag();	
  
 }
	//错误
 {
	const complex c1(2,1);
	cout<<c1.real();
	cout<<c1.imag();
 }

参数传递:pass by value vs.pass by reference(to const)


尽量使用ref(引用)传递。

value是复制了一份数据到栈上。

ref是直接传引用(引用本质上那还是指针,四个字节)。

所以传ref快,而且方便改值(尽管传char可能更小,一个字节,但是没必要这么细,通常来说传ref快)。

如果不想形参影响到实参(只是为了速度),加const关键字

complex& operator += (const complex&);

返回值传递:return by value vs.return by reference(to const)


返回值尽量使用引用返回(可以的情况下)。

friend(友元)


类中被定义为友元的函数,可以自由取得类中private权限的成员

class complex
{
...
private: 
	double re,im;

	friend complex& __doapl(complex*,const complex&);
}

inline complex&
__doapl (complex* ths,const complex& r)
{
 //此处直接取得并修改private权限中数据,不必通过public权限下函数
	ths->re += r.re;
	ths->im += r.im;
	return *ths;
}


友元提升了调用private中数据的速度,但打破了封装

相同class的各个objects互为friends(友元)


class complex
{
public:
...
	int func(const complex& param)
	{return param.re+param.im;}
}
{
	complex c1(2,1);
	complex c2;
//使用一个对象的成员函数来调用另一个对象的private权限下的数据
	c1.func(c2);	
}

class body外部各种定义(definitions)


首先,综上所述,定义一个类要注意的点

  • 数据写入private权限模块下

  • 函数的参数尽量以引用形式传入(reference),不想使传入的数据改变的情况下,reference前加const

  • 返回值同样尽量以引用形式传递

  • 不改变数据的成员函数后需跟const关键字,避免创建const对象时发生冲突

  • 构造函数使用其特有的结构赋初值

    complex(double r=0,double i=0):re(r),im(i){};

什么情况下可以pass by reference && 什么情况下可以return by reference


函数的返回值与传入的参数无关(需要开辟新空间表示返回值),则不可使用引用传递(因为在函数结束时,所开辟的额外空间即会被清除,引用传递会传出错误的内容);其余情况下均可返回引用值。

5.操作符重载与临时对象

operator overloading(操作运算符重载-1,成员函数) this


操作符实际就是一种函数。

任何成员函数都包含一个隐藏this指针,指向成员函数的调用者

//使用场景
{
	complex c1(2,1);
	complex c2(5);
 	c1+=c2;    		//此处使用运算符重载对complex类的'+='符号做定义
}

//类内定义部分
{
	complex::operator +=(*this,*const complex& r) //实际上,参数中的'this'被隐藏,为函数调用者
	{return __doapl(this,r);}
}

return by reference 语法分析


传递者无需知道接收者是以reference形式接收

inline complex& //此处以引用方式接收
__doapl(complex* ths,const complex& r)
{
	...
	return *ths;//此处返回指针中内容
}

重载运算符+=返回值为complex& or complex 而不直接使用void的意义在于,可能在使用过程中会面临c1+=c2+=c3此种连等的场景,如果返回值为void类型,则第一个+=不成立(因为c2+=c3部分的返回值为viod类型,无法满足c1+=c2部分需要一个complexorcomplex&类型的参数传入的需求),故设置返回值类型为前者。

class body之外的各种定义(definitions)


operator overloading(操作符重载-2,非成员函数) (无this)
//重载的运算符相同,但参数不同
complex operator+ (const complex& c1,const complex& c2);
complex operator+ (const complex& c1,int r);
complex operator+ (int r,const complex& c1);

//对应的不同运用场景
{
	complex c1(5,3);
	complex c2(5);
	complex c3;
	
	c3 = c1 + c2; 	//参数为两个object
	c3 = c1 + 5 ;	//参数为一个object和一个整数
	c3 = 5  + c1;  	//参数为一个整数和一个object(顺序不同)
}
temp object(临时对象) typename();

上面的函数绝对不可return by reference,因为它们的返回的必定是个local objecttypename + () 此种使用方法是正确的,叫做临时对象,例如complex (5)cout<<complex(5).getRe()<<endl;

<<(输出)的运算符重载

与常见的运算符重载不同,<<符号的重载存在一些特殊之处

注意:所有的运算符重载,均作用在操作符左边的对象之上。

complex c1(9,8);
cout<<c1;//大家更习惯于此种输出形式
c1<<cout;//如果<<符号的重载定义在类内,则被隐藏的complex指针存在于左边,则需以此种形式输出              

//此处函数声明写在complex.h文件中,若将声明写入complex.cpp中,需在.h文件中写入函数定义(类外)。
#include<iostream>
ostream& operator<<(ostream& os,const complex& c)
{
	os<<"("<<real(c)<<","<<imag(c)<<")";
	return os;
}

7.三大函数:拷贝构造,拷贝复制,析构

Class String


Big Three,三个特殊函数
class String
{
public:
	String(const char* cstr = 0); 			//构造函数
	//包含指针的类必定要包含copy ctor和copy op=
    String(const String& str);			  //拷贝构造函数,与构造函数函数名相同,传入参数为另一个同类对象
	String& operator = (const String& str); //拷贝赋值函数,对‘=’运算符的重载,特别之处在于传入参数为另一个同类对象
	~String();   //析构函数,在离开作用域时执行,释放对象

    char* get_c_str() const{return m_data;}
private:
    char* m_data;//因字符串数据量可能较大,故使用字符指针在进行数据的标识
}
ctor 和dtor(构造函数和析构函数)
//构造函数
inline String::String(const char* c_str = 0)
{
	if(c_str){
	//如果传入一个字符串,则需要开辟字符串长度+1(为了存放结束符号)的空间
        m_data = new char[strlen(c_str)+1];
		strcpy(m_data,c_str);
	}
	else{
	//如果传入的字符串为空,则在栈区开辟一块一个字节大小的空间存储'\0'结尾符号
		m_data = new char[1];
		*m_data = '\0';       
	}
}

//析构函数
inline String::~String()
{//需手动释放在栈区开放的新空间,否则会发生内存泄漏(深拷贝)
	delete[] m_data;
}

注意:不直接使得传入的char指针与新建的对象本身所包含的指针相等的原因是:会出现两个指针指向同一空间的问题,使得两个对象实质上包含统一块数据,修改其中一个会对另一个产生影响,没有达到新建对象的目的;与此同时,如果赋值前的指针原本包含内容,则原本的内容没有指针指向,发生内存泄漏(浅拷贝)

深拷贝与浅拷贝

copy assignment operator(拷贝赋值操作)
inline 
String& String::operator = (const String& str)
{	
	//检测自我赋值,此处&为取址符
	if(this == &str) return *this; 	
	
	delete[] m_data; 	//与拷贝构造的区别存在于此,因为赋值之前this指针指向的对象包含内容,故需先释放内存;
	m_data = new char[strlen(str)+1];
	strcpy(m_data,str)
	return *this;
}
一定要在operator = 中检查是否self assignment

自我检测

8.堆、栈与内存管理

所谓stack(栈),所谓heap(堆)


栈与堆的定义

class Complex{...};
...
//在作用域中分别在栈区和堆区以对象和指针形式创建两个对象;栈区创建的对象在作用域外自动释放,而由new在堆区开辟的空间需要自行手动释放
{
	Complex c1(1,2);				//c1所占用的空间来自栈
	Complex *p = new Complex(3);	//Complex(3)是个临时对象,占用的空间是以new从堆区动态分配而得,并由p指向
}

不同定义对象的生命期


对象类型生命期
static local objects整个程序
global objects整个程序(视为一种static objects)
heap object被delete 之后结束

注意: 如果当作用域结束,在堆区开辟的空间依然存在,但指针p的生命却已经结束,作用域之外再也看不到p(也就没机会delete p),则发生内存泄漏。

new:先分配memory,再调用ctor


Complex* pc = new Complex(2,1);

//当你在new的时候,编译器为你做的事情 
Complex *pc;
	void* mem = operator new(sizeof(Complex)); 	//分配,背后原理为调用malloc(n)函数
	pc = static_cast<Complex*>(men);			//转型
	pc->Complex::Complex(1,2);					//构造

delete:先调用dtor,再释放memory


String* ps = new String("Hello");
...
delete ps;

//delete时,编译器转化上面语句为
	String::~String();			//析构函数,释放字符串(指针指向)内容
	operator delete(ps);		//释放内存,释放字符串(指针)本身,其内部调用free(ps);

注意 array new需要搭配 array delete,即new typename[] 搭配delete[]

//正确的做法
	String* p = new String[3];
	delete[] p;                //换起3次dtor   

//错误的做法
	String *p = new String[3];
	delete p;   			   //换起1次dtor

array new一定要搭配array delete

10.扩展补充,类模板,函数模版,及其他

进一步补充:static


全局空间存在分别存储类中的方法静态成员变量静态成员函数的空间,以上三个内容在创建对象之前即存在。静态成员变量对于所有的对象共用一份;而静态成员函数修改所有对象共用的那份静态成员变量。

而正常的成员变量会在初始化object的时候创建。

  • 普通函数处理普通数据(需要传this pointer),当然也可以处理静态数据。

  • 静态函数不能处理普通数据,只能处理静态数据。

  • 静态数据除了在类里面做好声明,必须要在类外做好定义。

class Account{
publid:
    static double m_rate;          //静态数据在类内声明
	static void set_rate(const double& x){m_rate = x;}
}
double Account::m_rate = 8.0;      //静态数据必须类外定义,初始化与否不必须;

int main)_{
    Account::set_rate(5); //调用方法1:直接访问
    Account().set_rate(5); //调用方法2:通过实例访问
}
把ctors放在private区

Singleton中用到了静态变量(唯一的实例)、静态函数(get方法)。

优化方案:去掉静态类变量,放在静态函数里面;这样只有有人调用get方法的时候,实例才会被创建。

进一步补充:cout


为何cout可以对那么多的数据类型进行输出?因为cout 类继承自 ostream,而在源代码中,class ostream对诸多数据类型均进行了运算符重载。

cout能够输出多种类型数据的原因

进一步补充:class template,类模板


template<typename T>
class complex{
public:
	complex(T r=0,T i=0):re(r),im(i)
	{}
	T real() const{return re;}
	T imag() const{return im;}

private:
	T re,im;
	friend complex& __doapl (complex* ,complex&);

};

{
	complex<double> c1(2.5,1.5);	
	complex<int> c2(2,6);
}

进一步补充:function template,函数模版

与类模板有所区分的,函数模版在使用时不用指定数据的类型,编译器会自动根据数据类型进行推导,即引数推导(argument deduction)。

template<class T>
const T& min(const T &a,const T& b)
{	
	return b<a?b:a;	  //此处若类型T不为常见类型,则需要对<进行重载
}

{
	stone r1(2,3),r2(3,3),r3;
	r3 = min(r1,r2);
}

进一步补充:namespace

namespace的意义在于防止不同的代码书写者所编写的内容发生名称上的冲突,故不同的书写者在不同的namespace编写自己的代码。

namespace书写格式如下:

//所有的内容被包含在名为std的namespace中
namespace std
{
...
}

namespace调用有如下几种方式:

//using directive   全开
#include<iostream.h>
using namespace std;

int main()
{
	cin<<...;
	cout<<...;

	return 0;
}

//using declaration   仅开启使用的部分
#include<iostream.h>
using std::cin;

int main()
{
	cin<<...;	//已在抬头开启,故可直接使用
	std::cout<<...;  //未在抬头开启,需要在命令前加入空间名

	return 0;
}

11.组合与继承

Object Orient Programming,Object Orient Design


类与类之间的关系可分为如下三种

  • Inheritance(继承)

  • Composition(复合)

  • Delegation (委托)

Composition(复合),表示has-a


一个类的其中包含着另一个类,即一种has-a的关系,定义为复合。一个类对另一个功能强大的类进行改装(本类所需要的部分功能另一个强大的类已经拥有,只需要拿过来稍作修改(来为满足自身功能命名要求)便可使用)。

注意 生命周期同步

template <class T>
class queue{
	...
protect:
	deque<T> c; 	//底层容器,即该类中包含的另一个类
	...
}

类与类之间的关系-复合

另一个角度,从内存的角度理解复合

从内存角度理解复合

Composition(复合)下的构造和析构

复合关系中,本类与包含类为包含关系,即ContainerComponent的关系。

复合关系中的构造与析构

Delegation(委托).Composition by reference

一个类以引用的形式(实际上是指针但学术界叫做引用)包含另一个类,叫做Delegation(委托)又名Composition by reference(以引用复合)。

注意 两部分生命周期不同

//file String.hpp
class StringRap;
class String{
...
private:
	StringRep* rep;		 	//pimpl设计形式,又名handle/Body
}

Inheritance(继承),表示is-a


类(子类)与被继承的类(父类)之间的关系如图

类与被继承的类之间的关系

注意 子类可以继承父类所有的数据,但继承最有价值的点在于虚函数

struct _List_node_base
{
	_List_node_base* _M_next;
	_List_node_base* _M_prev;
};

template<typename _Tp>
struct _List_node
		:public: _List_node_base   //此处为继承的格式
{
	_Tp _M_data;
}
Inheritance(继承)关系下的构造和析构

继承关系下的构造与析构与复合的构造与析构类似,但不同的是继承中的base class的dtor必须是virtual,否则会出现undefined behavior

继承关系下的构造与析构

12.虚函数与多态

Inheritance(继承)with virtual function(虚函数)


在继承关系中,子类可以继承父类之中的数据(占用部分内存),同时继承父类中函数的调用权,即子类可以调用父类的函数。

类中的函数可以分为以下三种

函数类型解释
非虚函数(no-virtual父类不希望子类重新定义(override,覆写)的函数
虚函数(virtual)父类希望子类重新定义的函数,且函数应该有默认定义
纯虚函数(pure virtual)父类希望子类重新定义的函数,且函数没有默认定义
class Shape{
public: 
	virtual void draw() const = 0;	//纯虚函数,子函数一定要覆写
	virtual void error(const std::string& msg);//虚函数,子函数可以覆写
	int objectID() const;          //非虚函数,子函数无需覆写
	...
}

class Rectangle:public Shape{};
class Ellipse:public Shape{};

以下是关于常见的开启文件场景中运用继承关系的过程:

Template Method

13.面向对象设计经典案例

Delegation(委托)+ Inheritance(继承)结合的设计模式


案例1 Observer

设计目标:多个观察者显示同一份数据,数据发生变化则各个观察者看到的内容也随之变化。

Observer设计案例

代码实现:

class Subject
{
	int m_value;
	vector<Observe*> m_views;    //用来存放观察者对象
public:
	void attach(Observer* obs)	 //用来给予观察者观察权限
	{
		m_views.push_back(obs);
	}
	void set_val(int value)		
	{
	    m_value = value;		 //更新数据
		notify();				 //告知观察者数据已更新
	}
	void notify()				 
	{	//对发放观察权限的每个观察者所观察到的数据进行更新
		for(int i=0;i<m_views.size();++i)	
		m_views[i]->update(this,m_value);
	}
};

class Observer
{
public:
	virtual void updata(Subject* sub,int value) = 0;//留下纯虚函数接口,以便各种不同的观察方式进行覆写
}

对于SubjectObserver以及继承与observer,并以自己的数据观察方式观察的子类之间的关系图如下:

Subject与Observer及其子类之间的关系

案例2 Composite

Composite

案例3 Prototype

现在要去创建未来的类对象

Prototype

代码实现:

class Subject
{
	int m_value;
	vector<Observe*> m_views;    //用来存放观察者对象
public:
	void attach(Observer* obs)	 //用来给予观察者观察权限
	{
		m_views.push_back(obs);
	}
	void set_val(int value)		
	{
	    m_value = value;		 //更新数据
		notify();				 //告知观察者数据已更新
	}
	void notify()				 
	{	//对发放观察权限的每个观察者所观察到的数据进行更新
		for(int i=0;i<m_views.size();++i)	
		m_views[i]->update(this,m_value);
	}
};

class Observer
{
public:
	virtual void updata(Subject* sub,int value) = 0;//留下纯虚函数接口,以便各种不同的观察方式进行覆写
}

对于SubjectObserver以及继承与observer,并以自己的数据观察方式观察的子类之间的关系图如下:

Subject与Observer及其子类之间的关系

案例2 Composite

Composite

案例3 Prototype

现在要去创建未来的类对象

Prototype

后记

至此,《面向对象编程(上)》就学习就告一段落了,《c++程序设计(下)-- 兼谈对象模型》的高山等着我们前去攀登!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值