从SOD到OOD(C++风格程序设计)

https://www.runoob.com/
这是一个很好的网站,可以找到各种教程。

++++++++++++++++++++++++++++++++
this指针
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。this的目的总是指向这个对象,所以this是一个常量指针,我们不允许改变this中保存的地址
为了能让大家看清 this 指针的本质,我们会先讲一点 C++ 的历史——C++ 程序到C程序的翻译过程。 C++ 的编译器实际上是将 C++ 程序翻译成C语言程序,然后再用C语言编译器进行编译
C语言中只有全局函数,因此成员函数只能被翻译成全局函数
myCar.Modify();这样的语句也只能被翻译成普通的调用全局函数的语句。那如何让翻译后的 Modify 全局函数还能作用在 myCar 这个结构变量上呢?答案就是引入“this 指针”。
类被翻译成结构体,对象被翻译成结构变量,成员函数被翻译成全局函数。但是C程序的全局函数 SetPrice 比 C++ 的成员函数 SelPrice 多了一个参数,
就是struct CCar *this。
car.SetPrice(20000);被翻译成SetPrice(&car, 20000);

void Date::setMonth(  int mn )
{
    month = mn;  // 这三句是等价的
     this->month = mn;
    (* this).month = mn;
}

实际上,现在的C编译器从本质上来说也是按上面的方法来处理成员函数和对成员函数的调用的,即非静态成员函数实际上的形参个数比程序员写的多一个。多出来的参数就是所谓的“this指针”。这个“this指针”指向了成员函数作用的对象,在成员函数执行的过程中,正是通过“this指针”才能找到对象所在的地址,因而也就能找到对象的所有非静态成员变量的地址。

#include <iostream>
using namespace std;
class A
{
    int i;
public:
    void Hello(){ cout << "hello" << endl; }
};
int main()
{
    A* p = NULL;
    p -> Hello();
}

在上面的程序中,p 明明是一个空指针,为何通过它还能正确调用 A 的成员函数 Hello 呢?因为,参考上面 C++ 到C程序的翻译,P->Hello()实质上应该是Hello§,在翻译后的 Hello 函数中,cout 语句没有用到 this 指针,因此依然可以输出结果。如果 Hello 函数中有对成员变量的访问,则程序就会出错。
关键是,

P->Hello()实质上是Hello( P ),

静态成员函数并不作用于某个对象,所以在其内部不能使用 this 指针;否则,这个 this 指针该指向哪个对象呢?

+++++++++++++++++++++++++++++++
namespace
最常见的是
using namespace std;
这是使用标准库时,指定的名空间。

每个class都是一个自己的名空间,在定义class的成员函数的函数体时,要指定class的名空间。

+++++++++++++++++++++++++++++++
封装

封装是通过class来实现的。类中的数据和方法称为类的成员。
在C中,函数是全局的,但是C++中,函数可以是局域的。即作用于class-object的范围内。
在SOD风格设计中,所设计的struct,对应于OOD中的object,
在SOD设计中,设计的函数,是全局的,而且首参,必须是一个struct的指针,
在OOD设计中,设计的函数,是局域的,而且首参被默认为this指针,this指针是指向object的。
所以本质上,object就是一个struct,只不过,额外声明了一系列函数,这些函数和object的struct深度绑定,封装为class。

在调用风格上,略有区别。
SOD设计中,落脚点比较直观,就是函数调用时,指定的首参,就是某个struct,表示这个函数以该struct为核心数据变量,执行相应的处理。
OOD设计中,落脚点在object,首选选定一个object,以它为核心数据变量,然后找到合适的函数,对数据变量进行处理。

P->Hello()实质上是Hello( P ),

关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。
类的对象的公共数据成员可以使用直接成员访问运算符 (.) 来访问。
您也可以指定类的成员为 private 或 protected,私有的成员和受保护的成员不能使用直接成员访问运算符 (.) 来直接访问。

struct 和 class 最本质的一个区别就是访问控制,即作用域合法性判定,
类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。

public成员变量在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值。
private成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。如果您没有使用任何访问修饰符,类的成员将被假定为私有成员。
protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected成员在派生类中是可访问的。

实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数,

那么,C++是如何实现作用域合法性判定的呢?
我们知道,C++中,访问public成员数据,只能使用(.)运算符或者(->)运算符。这两个操作符被称为成员定位符。
而private成员数据或者成员函数,如果也使用成员定位符,则会出现作用域合法性失败。
所以,对private成员的调用或者访问,只能出现在同一个class的其他成员函数中。

对object的成员定位符操作,
编译器会翻译成
this->xxx_private_member,
并检查上下文,如果当前函数的作用域和this指针所指向的object的作用域一致,则合法性通过。

所以在设计private成员时,需要特别注意,必须要有其他的成员,去访问这些private成员,否则这些private成员,就成了zombie member。

调用链的起始,都是由public成员函数率先被调用的。

+++++++++++++++++++++++++++++++
继承

继承的本质,就是struct的扩展。
对于数据成员,就是先生成BASE类的struct,然后在后面追加derivate类的成员,构成更大的struct。
对于成员函数,就是更新作用域合法性判定规则。即访问控制。
derivate类和base类是不同的class,所以在derivate类的成员函数中,出现对base类的成员的调用或者变量访问时,会进行跨类作用域合法性检查。

派生类可以访问基类中所有的非私有成员。即public和protected。
因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private,并由BASE类的public或者protected成员来访问。

可以看出,
private成员,只能被本类的成员访问,派生类也不能访问,所以,设计private成员时,必须配套设计public或者protected成员,作为agent,对它进行调用或者访问,以防止它成为zombie member。
protected成员,能被本类成员访问,也能被派生类访问,设计protected成员的目的,就是为了让派生类去访问,否则,如果只想让本类成员访问,则设计成private更好。protected成员可以访问private,所以,它通常作为shadow agent来使用。向派生类提供访问接口。但是除了派生类,其他类不可见。
public成员,能被所有地方访问,设计public成员的目的,是为了让外部能够访问,它通常作为visible agent来使用。

多继承即一个子类可以有多个父类,它继承了多个父类的成员。

有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性,决定成员访问权限是否降级。这并不影响派生类去访问基类的public和protected。但是,这会影响到当派生类再被其他类访问时,派生类对BASE类的成员的访问控制。
1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
2.protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
3.private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private

例如,有如下三个类,base,derivate1, guest。
1 class derivate1 : public base
当guest访问derivate1的成员时,如果成员是来自于base,则仍旧保持其原有的访问权限不变。来自base的public成员,仍旧能被全局访问。

2 class derivate1 : protected base
当guest访问derivate1的成员时,如果成员是来自于base,则将public成员降级,成为protected成员,来自base的原本的public成员,将只能被后续的派生类derivate2访问,而不再能被全局访问。

3 class derivate1 : private base
当guest访问derivate1的成员时,如果成员是来自于base,则将public成员降级,成为private成员,将protected成员降级,成为private成员,来自base的原本的public成员,将只能被derivate1的成员访问,而不再能被全局访问,来自base的原本的protected成员,将只能被derivate1的成员访问,而不再能被后续的派生类derivate2访问。

从中可以看出,三种不同的继承方式,取决于设计目的。即访问阻断。
如果是public继承,则派生类并不想对BASE类进行访问阻断,所以不需要另行设计agent。直接由BASE类的成员函数向外提供全局服务。
如果是protected继承,则派生类需要对BASE类进行部分访问阻断,所以可以设计visible agent,由agent成员函数来访问BASE类,并向外提供全局服务。也可以不设计visible agent,使BASE类的成员函数只向后续派生类提供直系服务。
如果是private继承,则派生类需要对BASE类进行完全访问阻断,所以必须设计agent,以避免private成员成为zombie member。可以设计visible agent,由agent成员函数来访问BASE类,并向外提供全局服务,也可以设计shadow agent,由agent成员函数访问BASE类,并只向后续派生类提供直系服务。

+++++++++++++++++++++++++++++++
重载
在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。编译器通过把所使用的参数类型与定义中的参数类型进行比较,决定选用匹配的定义。
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。
不能仅通过返回类型的不同来重载函数。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }

+++++++++++++++++++++++++++++++
多态,虚函数

virtual是让子类与父类之间的同名函数有联系,这就是多态性,实现动态绑定。
任何类若是有虚函数就会比比正常的类大一点,所有有virtual的类的对象里面最头上会自动加上一个隐藏的,不让我知道的指针,它指向一张表,这张表叫做vtable,vtable里是所有virtual函数的地址。
多态特性的工作依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,那么该成员函数就变成了虚函数,
这里的vtable不是对象的,而是属于类的,这就是多态的实现机制。
调用同名函数却会因上下文的不同而有不同的实现。

Child chd;
Father *pFather = \&chd;
pFather->do_something();

编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding),
向上类型转换时,编译器认为指针pFather保存的就是父类对象的地址。当在main()函数中执行pFather->do_something()时,调用的当然就是Father对象的do_something函数。而不是child的do_something函数。
要解决这个问题就要使用迟绑定(late binding)技术。在基类中声明函数时使用virtual关键字,一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
template

模板(Templates)使得我们可以生成通用的函数,这些函数能够接受任意数据类型的参数,可返回任意类型的值,而不需要对所有可能的数据类型进行函数重载。这在一定程度上实现了宏(macro)的作用。

模板函数定义的一般形式如下所示:
template <typename type>
ret-type func-name(parameter list)
{
// 函数的主体
}
或者
template <class type>
ret-type func-name(parameter list)
{
// 函数的主体
}

例如:

template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
或者
template <class T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 

正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:
template <class type>
class class-name {
//类定义主体
}
例如:

template <class T>
class Stack { 
  private: 
    vector<T> elems;     // 元素 
 
  public: 
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
}; 

template <class T>
void Stack<T>::push (T const& elem) 
{ 
    // 追加传入元素的副本
    elems.push_back(elem);    
} 

template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 

template <class T>
void Stack<T>::pop () 
{ 
    if (elems.empty()) { 
        throw out_of_range("Stack<>::pop(): empty stack"); 
    }
    // 删除最后一个元素
    elems.pop_back();         
} 

在 C++ Template 中很多地方都用到了 typename 与 class 这两个关键字,而且好像可以替换,是不是这两个关键字完全一样呢?

相信学习 C++ 的人对 class 这个关键字都非常明白,class 用于定义类,在模板引入 c++ 后,最初定义模板的方法:
template<class T>…
这里 class 关键字表明T是一个类型,后来为了避免 class 在这两个地方的使用可能给人带来混淆,所以引入了 typename 这个关键字,它的作用同 class 一样表明后面的符号为一个类型,这样在定义模板的时候就可以使用下面的方式了:
template<typename T>…
在模板定义语法中关键字 class 与 typename 的作用完全一样。推荐使用typename。

typename 另外一个作用为:使用嵌套依赖类型(nested depended name),
例如:

class MyArray 
{ 
    public:
    typedef int LengthType;
.....
}

template<class T>
void MyMethod( T myarr ) 
{ 
    typedef typename T::LengthType LengthType; 
    LengthType length = myarr.GetLength; 
}

这个时候 typename 的作用就是告诉 c++ 编译器,typename 后面的字符串为一个类型名称,而不是成员函数或者成员变量,这个时候如果前面没有 typename,编译器没有任何办法知道 T::LengthType 是一个类型还是一个成员名称(静态数据成员或者静态函数),所以编译不能够通过。

函数模板可以重载,只要它们的形参表不同即可。
例如:

template<class T1, class T2>
void print(T1 arg1, T2 arg2)
{
  cout<<arg1<<" "<<arg2<<endl; 
}

template<class T>
void print(T arg1, T arg2)
{
  cout<< arg1<< " "<< arg2<< endl;
}

如果需要代码分离,即 template class 的声明、定义,以及 main 函数分属不同文件。
则 main.cpp 文件中需要同时包含 .h 文件和 .cpp 文件,不然会出现链接错误。

// main.cpp
#include "MyStack.h"
#include "MyStack.cpp"

// 其他include
// main函数主体 

更推荐的做法是,在H文件中include对应的CPP文件。
例如:

//statck.h
template <class T,int maxsize = 100> 
class Stack {
    public:
        Stack();
        ~Stack();
        void push(T t);
        T pop();
        bool isEmpty();
    private:
        T *m_pT;        
        int m_maxSize;
        int m_size;
};

#include "stack.cpp"

在CPP文件中去定义模板函数,
例如

//stack.cpp
template <class T,int maxsize> Stack<T, maxsize>::Stack(){
   m_maxSize = maxsize;      
   m_size = 0;
   m_pT = new T[m_maxSize];
}
template <class T,int maxsize>  Stack<T, maxsize>::~Stack() {
   delete [] m_pT ;
}
        
template <class T,int maxsize> void Stack<T, maxsize>::push(T t) {
    m_size++;
    m_pT[m_size - 1] = t;
    
}
template <class T,int maxsize> T Stack<T, maxsize>::pop() {
    T t = m_pT[m_size - 1];
    m_size--;
    return t;
}
template <class T,int maxsize> bool Stack<T, maxsize>::isEmpty() {
    return m_size == 0;
}

从中可以看出,
模板可以有类型参数,也可以有常规的类型参数int,并为其设置默认模板参数值。

设计模板函数或者模板类时,遵循先具体后抽象的原则。
即,先设计出一个具体的函数或者类,
然后,从函数或者类中,找出想要抽象化(宏化)的类型,用抽象符号来替代,并在头部的template声明中,登记下来,再找出想要抽象化(宏化)的参数值,用抽象符号来替代,并在头部的template声明中,登记下来。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
设计实践:
将SOD代码用OOD改造
在SOD设计时,H文件中会定义struct,以及static变量。C文件中,会定义操作函数function,这些操作函数,首参都是关联的struct,
改造成OOD设计是,H文件中将struct和关联的操作函数function,封装到class的定义中。在CPP文件中,再来定义这些class::function。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值