C++ 程序设计的要素

一、建模

1. 抽象

2. 关键对象的建模

二、隐藏实现细节

1. 物理隐藏:声明与定义

将内部细节(.cpp)和公有接口(.h)分离,存储在不同文件中

2. 逻辑隐藏:封装

使用C++中语言受保护的和私有的访问控制特性,从而限制访问内部细节

  • 隐藏成员变量:成员变量始终声明为私有的,而不是受保护的和公有的
  • 隐藏不必要的实现方法
    • 永远不要返回私有数据成员的非const指针或引用,这会破坏封装性
    • 将私有函数声明为.cpp文件中的静态函数,而不要将其作为私有方法暴露在公开的头文件中(更好的做法是使用Pimpl惯用法
  • 隐藏实现类:隐藏那些存粹是为了实现的类

三、最小完备

1. 不要过度承诺

在发布了某个API并且已经有用户使用之后,增加新功能很容易,而删除功能就很困难,当不确定是否需要某个接口时,就不要提供此接口(疑惑之时,果断弃之!精简API中公有的类和函数)

2. 谨慎添加虚函数

  • 避免将函数声明为可重写的函数(虚函数),除非你有合理且迫切的需求
  • 一般来说,如果API没有在其内部调用某个特定的方法,那么此方法很可能不应该声明为虚方法,当潜在的子类与基类之间形成一种“is-a”的关系时,继承才有意义
  • 使用虚函数的注意点:
    • 如果基类中包含任意虚函数,那么需要将析构函数声明为虚函数(确保子类可以释放其申请的额外资源)
    • 编写文档,说明虚函数内部实现中需要调用哪些方法以维持对象内部的完整性,子类继承时需要按照文档实现
    • 不要在构造函数或析构函数中调用虚函数,这些调用不会指向子类

3. 便捷API

基于最小化的核心API,以独立的模块或库的形式构建便捷API

四、易用性

1. 可发现性

可发现的API要求用户可以通过API自身明白如何使用它,而不需要参阅任何解释或文档

2. 不易误用

  1. 最常见的误用API的方式是向方法传递错误的参数或非法值。当方法拥有相同的类型的参数,但用户忘记参数顺序或者使用int而不是enum类型时,误用情况很可能发生
//容易发生误用的写法
std::string FindString(const std::string& text,bool bSearchForward,bool bCaseSensitive);
//不易发生误用的写法
enum SEARCHFORWARD
{
	FORWARD,
	BACKWARD
}
enum CASESENSITIVE
{
	CASE_SENSITIVE,
	CASE_INSENSITIVE
}
std::string FindString(const std::string& text,SEARCHFORWARD direction,bool aseSensitive);
  1. 对于enum不能解决的复杂情况,为了确保每个参数都有唯一的类型,可以引入新的类
//容易发生误用的写法
class Date
{
public:
	Date(int year,int month,int day);
}
//不易发生误用的写法
class Year
{
public:
	explicit Year(int y) : m_year(y) {}
	int GetYear() const { return m_year; }

private:
	int m_year;
};

class Month
{
public:
	explicit Month(int m) : m_month(m){}
	int GetMonth() const { return m_month; }

	static Month Jan() { return Month(1); }
	static Month Feb() { return Month(2); }
	static Month Mar() { return Month(3); }
	static Month Apr() { return Month(4); }
	static Month May() { return Month(5); }
	static Month Jun() { return Month(6); }
	static Month Jul() { return Month(7); }
	static Month Aug() { return Month(8); }
	static Month Sep() { return Month(9); }
	static Month Oct() { return Month(10); }
	static Month Nov() { return Month(11); }
	static Month Dec() { return Month(12); }

private:
	int m_month;
};

class Day
{
public:
	explicit Dat(int d) : m_day(d){}
	int GetDay() const { return m_day; }

private:
	int m_day;
}

class Date
{
public:
	Date(const Year& y, const Month& m, const Day& d);
};

Date BirthDay(Year(1992),Month::Sep,Day(23));

知识点:

1.使用枚举类型代替bool或者int类型,提高代码的可读性

2.避免使用拥有多个相同类型参数的函数

3. 一致性

  • 使用一致的函数命名和参数顺序
  • 拥有相似角色的类应该提供相似的接口,比如:std::map std::vector 都有接口size()

4. 正交

  • 正交的API意味着函数没有副作用
  • 调用设置特定属性的方法应该仅改变那个属性,而不能额外改变其他可以公共访问的属性
//不好的设计
class ShowerController
{
public:
	float GetTemperature() const { return m_temperature; };
	float GetPower() const { return m_power; };
	void SetPower(float p)
	{
		if (p < 0) p = 0;
		if (p > 100) p = 100;
		m_power = p;
		m_temperature = 42.0f + sin(p / 38.0f) * 45.0f;
	}

private:
	float m_temperature;			//温度
	float m_power;					//水压
};

//正交设计 
class ShowerController
{
public:
	float GetTemperature() const { return m_temperature; };
	float GetPower() const { return m_power; };
	void SetPower(float p)
	{
		if (p < 0) p = 0;
		if (p > 100) p = 100;
		m_power = p;
	}
	void SetTemperature(float t)
	{
		if (t < 42) t = 42;
		if (t > 85) t = 85;
		m_temperature = t;
	}

private:
	float m_temperature;			//温度
	float m_power;					//水压
};

5. 健壮的资源分配

常见的误用指针引发的错误:

  • 对NULL解引用:尝试对NULL指针使用->或*操作
  • 二次释放:对同一块内存调用两次delete或free()
  • 访问非法内存区域:对尚未分配或已经释放的内存使用->或*操作
  • 混用内存分配器:用delete释放由malloc()分配的内存,或用free()释放由new分配的内存
  • 数组释放不正确:使用delete操作符而非delete[] 释放数组
  • 内存泄漏:内存使用后没有释放

知识点:

1.使用智能指针:

  • 如果函数返回了需要用户释放的指针时
  • 用户需要的指针的生命周期比对象的生命周期更长
#include <memory>

using MyObjectPtr = std::shared_ptr<class MyObject>

class MyObject
{
public:
	static MyObjectPtr CreateInstance();
	~MyObject();

private:
	//用工厂方法创建实例
	MyObject();
};

MyObjectPtr MyObject::CreateInstance()
{
	return MyObjectPtr(new MyObject());
}

int main(int argc, int* argv[])
{
    //当ptr 变量超出作用域时 两个实例都会销毁(智能指针)
    //如果此处返回 MyObject* 类型  需要手动释放两个对象
	MyObjectPtr ptr = MyObject::CreateInstance();
	ptr = MyObject::CreateInstance();
	return 0;
}

2.将资源的申请和释放当作对象的构造和析构

  • 资源分配是对象构造,资源释放是对象析构
#include <string>
#include <mutex>

std::mutex mutex;

//错误写法
void SetName(const std::string& name)
{
	mutex.lock();

	if (name.empty())
		return;

	mutex.unlock();
}

//正确写法
void SetName(const std::string& name)
{
	std::lock_guard<std::mutex> guard(mutex);
	if (name.empty())
		return;
}

6. 独立平台

不要将平台相关的#if或#ifdef语句放到公共API中,因为这些语句暴露了实现细节,并使API因平台而异

#include <string>

//不好的设计 
//导致在不同的平台上创建不同的API,进而强迫API的用户为其应用程序引入同样的平台相关特征
//用户在使用GetGPSLocation 时也需要进行平台判断
class MobilePhone
{
public:
	bool StartCall(const std::string& number);
	bool EndCall();

#if defined TARGET_OS_IPHONE
	bool GetGPSLocation(double* lat, double* lon);
#endif // defined TARGET_OS_IPHONE
};

//好的设计
class MobilePhone
{
public:
	bool StartCall(const std::string& number);
	bool EndCall();
	bool HasGPS() const;
	bool GetGPSLocation(double* lat, double* lon);
};

bool MobilePhone::HasGPS() const
{
#if defined TARGET_OS_IPHONE
	return true;
#else
	return false;
#endif // defined TARGET_OS_IPHONE
}

五、松耦合

  • 耦合:软件组件之间相互连接的强度的度量,即系统中每个组件对其他组件的依赖程度
  • 内聚:单个软件组件内的各种方法相互关联或者聚合强度的度量

知识点:

优秀的API表现为松耦合和高内聚

1. 仅通过名字耦合

如果类A中仅需要知道类B的名字,即它不需要知道类B的大小或者调用类B的任何方法,那么类A就不需要依赖类B的完整声明(可以为类B使用前置声明)

class MyObject;   //只需知道MyObject 的名字

class MyObjectHolder
{
public:
    MyObjectHolder();

    void SetObject(MyObject* obj);
    MyObject* GetObject() const;

private:
    MyObject* obj;
};

//如果相关联的从cpp 文件仅仅存储并返回MyObject的指针,同时限制任何除指针比较外
//的与该指针的交互,就不需要#include "MyObject.h"

知识点:

除非确实需要#include类的完整定义,否则应该为类使用前置声明

2. 降低类耦合

知识点:

与成员函数相比,使用非成员、非友元的方法能降低耦合度


//耦合度高
class MyObject
{
public:
   std::string GetName() const;
   //类的成员函数,可以访问MyObject所有私有和受保护的成员函数以及数据成员
   //如果MyObject 有基类,则也可以访问基类中受保护的成员
   void PrintName() const;

private:
    std::string m_name;
};

//低耦合设计
class MyObject
{
public:
   std::string GetName() const;

private:
    std::string m_name;
};

 void PrintName(const MyObject& obj);

 //为了更好的传达MyObject和PrintName在概念上的关联性
 //1.可以将它们放到同一个命名空间中
 //2.可以创建一个新的帮助类的静态方法

//1
 namespace MyObjectHelper
 {
    void PrintName(const MyObject& obj);
 } // namespace MyObjectHelper
 
 //2
 class MyObjectHelper
 {
 public:
     static void PrintName(const MyObject& obj);
 };
 

3. 刻意的数据冗余

#include "ChatUser.h"
#include <string>
#include <vector>

//此类中 TextChatLog 与 ChatUser 耦合 但是此处实际上只使用了 userName(引入ChatUser实际上只是用来获取username)
class TextChatLog
{
public:
    bool AddMessage(const ChatUser& user, const std::string& msg);
    //获取聊天数量
    int GetCount()const;
    //获取聊天信息    类似 dujinwei[10:00]  hello    
    std::string GetMessage(int index);

private:
    struct ChatEvent
    {
        ChatUser m_user;                //用户信息
        std::string m_message;          //消息
        size_t m_timeStamp;             //时间戳
    };

    std::vector<ChatEvent> m_chatEvents;
};

//去除TextChatLog 与 ChatUser 耦合
//这种做法的弊端是在TextChatLog和ChatUser中都保留了用户名这个属性
//如果当前有个需求,修改用户名,并在历史信息中同步更新新用户名,就必须使用上面紧耦合版本,
//或者更改用户名的时候同步更改历史消息中的用户名
#include <string>
#include <vector>

class TextChatLog
{
public:
    bool AddMessage(const std::string& user, const std::string& msg);
    //获取聊天数量
    int GetCount()const;
    //获取聊天信息    类似 dujinwei[10:00]  hello    
    std::string GetMessage(int index);

private:
    struct ChatEvent
    {
        std::string m_userName;         //用户名
        std::string m_message;          //消息
        size_t m_timeStamp;             //时间戳
    };

    std::vector<ChatEvent> m_chatEvents;
};

4. 管理器类

知识点:

管理器类可以通过封装几个低层次的类降低耦合

5. 回调、观察者和通知

回调函数、观察者模式、信号槽通知

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值