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),并使用对象中所包含的函数来对数据进行处理
complex类(不含指针)与string类(内含指针)的区别
Object Based(基于对象)vs Object Oriented(面向对象)
- Object Based: 面对的是单一的class的设计
- Object Oriented:面对的是多重classes的设计,classes和classes之间的关系。
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
部分需要一个complex
orcomplex&
类型的参数传入的需求),故设置返回值类型为前者。
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 object。typename
+ ()
此种使用方法是正确的,叫做临时对象,例如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
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
对诸多数据类型均进行了运算符重载。
进一步补充: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(复合)下的构造和析构
复合关系中,本类与包含类为包含关系,即Container与Component的关系。
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{};
以下是关于常见的开启文件场景中运用继承关系的过程:
13.面向对象设计经典案例
Delegation(委托)+ Inheritance(继承)结合的设计模式
案例1 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;//留下纯虚函数接口,以便各种不同的观察方式进行覆写
}
对于Subject和Observer以及继承与observer,并以自己的数据观察方式观察的子类之间的关系图如下:
案例2 Composite
案例3 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;//留下纯虚函数接口,以便各种不同的观察方式进行覆写
}
对于Subject和Observer以及继承与observer,并以自己的数据观察方式观察的子类之间的关系图如下:
案例2 Composite
案例3 Prototype
现在要去创建未来的类对象
后记
至此,《面向对象编程(上)》就学习就告一段落了,《c++程序设计(下)-- 兼谈对象模型》的高山等着我们前去攀登!