忧郁的大能猫
好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
The passage uses the analogy of hiring contractors to illustrate how APIs let developers delegate complex tasks, avoid dealing with low-level details, and integrate specialized components—much like building a house by coordinating experts rather than doing everything yourself.
A C++ API will therefore generally include the following elements:
1. Headers/Modules: A collection of .h header files or module interface units that define the interface and allow client code to be compiled against that interface.
2. Libraries: One or more static or dynamic library files that provide an implementation for the API (e.g., .a, .lib, .dll, .dylib, etc. files).
3. Documentation
4. Example(It's better to have it.)
In fact, many software engineers prefer to expand the acronym API as abstract programming interface instead of application programming interface.
TIP: An API is a logical interface to a software component that hides the internal details required to implement it.
TIP: An API describes software used by other engineers to build their applications. As such, it must be well-designed, documented, regression tested, and stable between releases.
Hides implementation. By hiding the implementation details of your module, you gain the flexibility to change the implementation at a future date without causing an upheaval for your users.
Increases longevity.
Promotes modularization.
Reduces code duplication.
Removes hardcoded assumptions.
Easier to change the implementation.
Easier to optimize.
Code reuse is the use of existing software to build new software. It’s one of the holy grails of modern software development. APIs provide a mechanism to enable code reuse.
In essence, software development has become a lot more modular with the use of distinct components that form the building blocks of an application and talk together via their published APIs.
One of the difficulties in achieving code reuse, however, is that you must often come up with a more general interface than you originally intended.
Good APIs let developers work independently by providing stable contracts that hide implementation details and support parallel development.
For example, let's say that you are working on a string encryption algorithm that another developer wants to use to write data out to a configuration file.
#include <string.h>
class StringEncryptor
{
public:
/// set the key to use for the Encrypt() and Decrypt() calls
void SetKey(const std::string &key);
/// encrypt an input string based upon the current key
std::string Encrypt(const std::string &str) const;
/// decrypt a string using the current key - calling
/// Decrypt() on a string returned by Encrypt() will
/// return the original string for the same key.
std::string Decrypt(const std::string &str) const;
};
void StringEncryptor::SetKey(const std::string &key)
{}
std::string StringEncryptor::Encrypt(const std::string &str)
{
return str;
}
std::string StringEncryptor::Decrypt(const std::string &str)
{
return str;
}In this way, your colleague can use this API and proceed with the work without being held up by your progress.
The important point is that you have a stable interfaceda contractdon which you both agree, and that it behaves appropriately, such as Decrypt(Encypt("Hello")) == "Hello".
OS APIs. Every OS must provide a set of standard APIs to allow programs to access OS-level services.
Language APIs. The C language is supported by the C Standard Library, implemented as the libc library and associated man pages.
Image APIs. Gone are the days when developers needed to write their own image reading and writing routines.
3D graphics APIs. The two classic real-time 3D graphics APIs are OpenGL and DirectX.
GUI APIs. Any application that wants to open its own window needs to use a GUI tool kit. This is an API that provides the ability to create windows, buttons, text fields, dialogues, icons, menus, and so on.Some popular C/C++ GUI APIs include the wxWidgets library, Qt Company’s Qt API, GTKþ, and X/Motif.

File Formats = Rules for Saving and Loading Data
A file format defines a standard way to store data on disk.
Example: JPEG/JFIF defines exactly how image data and headers are arranged in a .jpg file.
Network Protocols = Rules for Sending Data Over the Network
Client–server systems or peer-to-peer systems exchange data using an agreed-upon network protocol.
Examples:HTTP、FTP
How File Formats / Protocols resemble APIs?
They look like APIs because they define a standard interface for information exchange:
- They specify what data looks like.
- They must remain backward compatible.
- Changes must respect existing users.
But they are not APIs because they are not code you link into your program.
TIP: Whenever you create a file format or client/server protocol, you should also create an API for it(创建对应的API接口去操作他们). This allows the details of the specification, and any future changes to it, to be centralized and hidden.
An API should provide a logical abstraction for the problem that it solves. That is, it should be formulated in terms of high-level concepts that make sense in the chosen problem domain, rather than exposing low-level implementation issues. You should be able to give your API documentation to a nonprogrammer, and that person should be able to understand the concepts of the interface and how it’s meant to work.
For example, let’s consider an API for a simple address book program. Conceptually, an address book is a container for the details of multiple people.
It seems logical, then, that our API should provide an AddressBook object that contains a collection of Person objects.
This initial design can be represented visually using a notation called Unified Modeling Language (UML)

An API should also model the key objects for the problem domain. This process is often called object-oriented design or object modeling because it aims to describe the hierarchy of objects in the specific problem domain. The goal of object modeling is to identify the collection of major objects, the operations they provide, and how they relate to each other.

The main reason to develop an API is to solve some related set of problems. So, it must be clear to your users how they can apply your design to achieve that goal. We just saw how you can model the key objects for your problem domain, but the users of your API also need to know how to create those objects and how to call the methods on those objects to complete their tasks.
To design a good API, you must demonstrate how it solves the core problems of the domain. Sequence diagrams help by showing step-by-step how objects interact to achieve real tasks. This reveals missing functions and ensures the API is usable for its intended purpose.
TIP: Physical hiding means storing internal details in a separate file (.cpp) from the public interface (.h).
C++20. This feature modules provides an alternative to the use of header files. However, you can still perform physical hiding with modules by putting your declarations in your module interface unit and definitions in your module implementation unit.
TIP: Encapsulation is the process of separating the public interface of an API from its underlying implementation.
TIP: Logical hiding means using the C++ language features of protected and private to restrict access to internal details
Someof the additionalbenefits of using getter/setter routines, rather than directly exposing member variables, include:
* Validation.
* Lazy evaluation.
* Caching.
* Extra computation.
* Notifications. 数据变了可以通知其他模块
* Debugging.
* Synchronization.
* Finer access control.
* Maintaining invariant relationships.
If you make a variable protected, then it can be accessed directly by any clients that subclass your class, and then exactly the same arguments apply as for the public case.
TIP: Data members of a class should always be declared private, never public or protected.
TIP: Never return non-const pointers or references to private data members. This breaks encapsulation.
TIP: Prefer declaring private functionality as static functions within the .cpp file rather than exposing them in public headers as private methods. (Using the Pimpl idiom is even better, though).
Some classes exist only to support your implementation and should not be exposed in the public API. Exposing them:
- Increases API complexity
- Risks users depending on internal details
- Makes future refactoring harder
- Hiding implementation classes keeps your API clean, maintainable, and stable.
- Users should only see what they need to use, not internal helpers.
- This allows you to change internal structures freely without breaking client code.
TIP: Remember Occam’s razor: plurality should not be posited without necessity.
The key point is that once you release an API and have clients using it, adding new functionality is easy but removing functionality is really difficult. The best advice then is: when in doubt, leave it out
TIP: When in doubt, leave it out! Minimize the number of public classes and functions in your API
Some common ways to address code duplication are:
- Function Refactoring
- Class Abstractions
- Dependency Injection: This is where the functionality is defined in a single object and that object is passed into the method calls where the functionality is needed.
- Automation: In cases where duplication is unavoidable, you can define a single source of truth for the functionality and write automation tools to generate the other code from that source of truth.
A good way to think about this is to ask yourself: if you needed to make a change to how something works, would you have to update multiple places in your code to make that change?
On the one hand, there’s the argument that an API should provide only one way to perform one task.This ensures that the API is minimal, singularly focused, consistent,and easy to understand.
However, on the other hand, there is also the argument that an API should make simple things easy to do.
Real-world Example: OpenGL / GLU / GLUT
OpenGL (Core API)
- Low-level primitives: points, lines, polygons.
- Very powerful but verbose; e.g., creating a sphere requires manually defining polygons.
GLU (Convenience API)
- Built on top of OpenGL.
- Provides higher-level functions like quadric surfaces, camera positioning, and sphere creation.
GLUT (Even Higher-Level)
- Another layer on top of OpenGL/GLU.
- Provides window management, event handling, and pre-defined geometric primitives.
Summary
- Core API: Minimal, primitive, consistent.
- Convenience API: Optional, higher-level wrappers for common tasks.
- Keep convenience separate to maintain focus and reduce complexity.
- This strategy improves usability for a wide range of clients without compromising maintainability.
TIP: Add convenience APIs as separate modules or libraries that sit on top of your minimal core API.(接口的易用性和基础性的结合)
you should be aware of the potential pitfalls:
- You can implement seemingly innocuous changes to your base classes that have a detrimental impact on your clients.
- Your clients may use your API in ways that you never intended or imagined.
- Clients may extend your API in incorrect or error-prone ways. For example, you may have a thread-safe API but, depending upon your design, a client could override a virtual function and provide an implementation without performing the appropriate mutex locking operations, opening the potential for difficult-to-debug race conditions.
- Overridden functions may break the internal integrity of your class.
In addition to these API-level behavioral concerns, there are the standard matters about which you should be aware when using virtual functions in C++:
- Virtual function calls must be resolved at runtime by performing a vtable lookup, whereas nonvirtual function calls can be resolved at compile time.
- The use of virtual functions increases the size of an object, typically by the size of a pointer to the vtable.(对创建的大量小对象有影响)
- Adding, reordering, or removing a virtual function will break binary compatibility.
- Virtual functions cannot always be inlined.
TIP: Avoid declaring functions as overridable (virtual) until you have a valid and compelling need to do so
A well-designed API should make simple tasks easy and obvious. For example, it should be possible for a client to look at the method signatures of your API and be able to glean how to use it without any additional documentation.
A discoverable API is one in which users can work out how to use the API on their own without any accompanying explanation or documentation.
TIP: Prefer the use of enums over Booleans and integers to improve code readability.
// bad
std::string FindString(const std::string &text,
bool search_forward,
bool case_sensitive);
FindString(text, true, false);
// good
enum class SearchDirection {
Forward,
Backward
};
enum class CaseSensitivity {
Sensitive,
Insensitive
};
std::string FindString(const std::string &text,
SearchDirection direction,
CaseSensitivity case_sensitivity);
FindString(text, SearchDirection::Forward, CaseSensitivity::Insensitive);TIP: Avoid functions with multiple parameters of the same type.
Using this design, clients can create a new Date object with the following unambiguous and easy to understand syntax. Also, any attempts to specify the values in a different order will result in a compile-time error:
// bad
class Date
{
public:
Date(int year, int month, int day);
...
};
Data(2012,12,12);
// good
class Year
{
public:
explicit Year(int y) :
mYear(y)
{}
int GetYear() const { return mYear; }
private:
int mYear;
};
class Month
{
public:
explicit Month(int m) :
mMonth(m)
{}
int GetMonth() const { return mMonth; }
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 mMonth;
};
class Day
{
public:
explicit Day(int d) :
mDay(d)
{}
int GetDay() const { return mDay; }
private:
int mDay;
};
class Date
{
public:
Date(const Year &y, const Month &m, const Day &d);
...
};
Date birthday(Year(1976), Month::Jul(), Day(7));A consistent API:
* Uses uniform naming
* Keeps parameter order predictable
* Provides consistent interfaces across related classes
* Follows familiar language/platform idioms
* Avoids duplicate ways to perform the same task
// 两个函数的srd dist参数的顺序不一致,很容易出错
void bcopy(const void *s1, void *s2, size_t n);
char *strncpy(char *restrict s1, const char *restrict s2, size_t n);
// 一个是size bytes,一个是(count * size) bytes,参数含义不连续
void *malloc(size_t size);
void *calloc(size_t count, size_t size); TIP: Use consistent function naming and parameter ordering.
In terms of API design, orthogonality means that methods do not have side effects.
As a result, making a change to the implementation of one part of the API should have no effect on other parts of the API
Furthermore, code that does not create side effects, or relies upon the side effects of other code, is much easier to develop, test, debug, and change because its effects are more localized and bounded.
An example:
Perhaps you’ve stayed in a motel where the controls of the shower are very unintuitive. You want to be able to set the power and the temperature of the water, but instead you have a single control that seems to affect both properties in a complex and nonobvious way.
也就是你调水量大小温度也会跟着变,反之亦然。
class CheapMotelShower
{
public:
float GetTemperature() const; // units = Fahrenheit
float GetPower() const; // units = percent, 0..100
void SetPower(float p);
private:
float mTemperature;
float mPower;
};
float CheapMotelShower::GetTemperature() const
{
return mTemperature;
}
float CheapMotelShower::GetPower() const
{
return mPower;
}
void CheapMotelShower::SetPower(float p)
{
if (p < 0) {
p = 0;
}
if (p > 100) {
p = 100;
}
mPower = p;
mTemperature = 42.0f + sin(p / 38.0f) * 45.0f;
}TIP: An orthogonal API means that functions do not have side effects.
One of the trickiest aspects of programming in Cþþ is memory management.
most C++ bugs arise from some kind of misuse of pointers or references, such as:
* Null dereferencing: trying to use -> or * operators on a nullptr.
* Double freeing: calling delete or free() on a block of memory twice.
* Accessing invalid memory: trying to use -> or * operators on a pointer that hasnot yet been allocated or that has already been freed.
* Mixing allocators: using delete to free memory that was allocated with malloc(), orusing free() to return memory allocated with new.
* Incorrect array deallocation: using the delete operator, instead of delete [], to freean array.
* Memory leaks: not freeing a block of memory when you are finished with it.
If your API involves any resource allocation, you should:
- Provide a wrapper class where
constructor = acquire resource
destructor = release resource
- Optionally provide a Release() method for manual control.
- Keep destructors non-throwing (they must never raise exceptions).
TIP: Return a dynamically allocated object using a smart pointer wherever possible.
TIP: Think of resource allocation and deallocation as object construction and destruction.
A well-designed Cþþ API should always avoid platform-specific #if/#ifdef lines in its public headers that produce different APIs on different platforms or that produce different APIs in debug versus release builds.
// bad:This poor design creates a different API on different platforms.
class MobilePhone
{
public:
bool StartCall(const std::string &number);
bool EndCall();
// 下面的把现实给暴露出来了
#if defined TARGET_OS_IPHONE
bool GetGPSLocation(double &lat, double &lon);
#endif
};
// good
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
}TIP: Never put platform-specific #if or #ifdef statements into your public APIs. It exposes implementation details and makes your API behave differently on differently platforms
TIP: Good APIs exhibit loose coupling and high cohesion.
One way to think of coupling is that given two components, A and B, how much code in B must change if A changes.
If class A only needs to know the name of class B (i.e., it does not need to know the size of class B or call any methods in the class), then class A does not need to depend upon the full declaration of B.In these cases, you can use a forward declaration for class B rather than including the entire interface
TIP: Use a forward declaration for a class unless you actually need to #include its full definition.
TIP: Prefer using nonmember nonfriend functions instead of member functions to reduce coupling.
// bad
class MyObject
{
public:
void PrintName() const;
std::string GetName() const;
...
protected:
...
private:
std::string mName;
...
};
// good, PrintName()只需要用MyObject的接口,不需要知道它的实现
class MyObject
{
public:
std::string GetName() const;
...
protected:
...
private:
std::string mName;
...
};
void PrintName(const MyObject &obj);Normally, good software engineering practice aims to remove redundancy: to ensure that each significant piece of knowledge or behavior is implemented only once (Pierce, 2002). However, reuse of code implies coupling, and sometimes it’s worth the cost to add a small degree of duplication to sever an egregious coupling relationship
TIP: Data redundancy can sometimes be justified to reduce coupling between classes.
// bad, 依赖于ChatUser
class TextChatLog
{
public:
bool AddMessage(const ChatUser &user, const std::string &msg);
int GetCount() const;
std::string GetMessage(int index);
private:
struct ChatEvent
{
ChatUser mUser;
std::string mMessage;
size_t mTimestamp;
};
std::vector<ChatEvent> mChatEvents;
};
// good
class TextChatLog
{
public:
bool AddMessage(const std::string &user, const std::string &msg);
int GetCount() const;
std::string GetMessage(int index);
private:
struct ChatEvent
{
std::string mUserName; // 增加了冗余,但减少了耦合
std::string mMessage;
size_t mTimestamp;
};
std::vector<ChatEvent> mChatEvents;
};A manager class is one that owns and coordinates several lower-level classes. This can be used to break the dependency of one or more classes upon a collection of low-level. classes.
This is often implemented using a Facade design pattern, which I’ll cover in the chapter on Patterns, or a Mediator design pattern. The difference between the two is that a Fac¸ade exposes only existing functionality in a different way whereas a Mediator adds new functionality.
before

after

TIP: Manager classes can reduce coupling by encapsulating several lower-level classes. They can be implemented using the Fac¸ade or Mediator design patterns.
最主要的是可以让low-level的代码在适当时机调用high-leve的代码。
可以使两个模块的耦合更低
1. Callback Functions (C-Style)
A callback is simply a function pointer passed into another module.
Module A gives Module B a function pointer.
Module B calls it when needed, without depending on A's headers.
This breaks cyclic dependencies and keeps modules decoupled.
Basic C++11 callback example
class ModuleB {
public:
using CallbackType = void (*)(const std::string&, void*);
// The `closure` is arbitrary user data (context), passed through to the callback.
void SetCallback(CallbackType cb, void* closure) {
mCallback = cb;
mClosure = closure;
}
void InvokeCallback(const std::string& name) {
if (mCallback) {
mCallback(name, mClosure);
}
}
private:
CallbackType mCallback = nullptr;
void* mClosure = nullptr;
};2. Observer Pattern (Object-Oriented Callbacks)
The observer pattern uses objects, not raw function pointers.
You define an abstract base class:
class Observable {
public:
virtual ~Observable() {}
virtual void CallbackMethod(const std::string&) = 0;
};A concrete observer implements it:
class MyObserver : public Observable {
public:
MyObserver(int data) : mData(data) {}
void CallbackMethod(const std::string& name) override {
std::cout << name << ": " << mData << "\n";
}
private:
int mData;
};Module B stores the observer:
class ModuleB {
public:
void SetObserver(std::shared_ptr<Observable> cb) {
mObserver = cb;
}
void InvokeObserver(const std::string& name) {
if (mObserver) mObserver->CallbackMethod(name);
}
private:
std::shared_ptr<Observable> mObserver;
};3. Notifications (Signals and Slots / Event Buses)
Callbacks and observers usually:
* involve one sender
* and one receiver
Example with boost::signals2
class MySlot {
public:
void operator()() const {
std::cout << "MySlot called!" << std::endl;
}
};
MySlot slot;
boost::signals2::signal<void()> signal;
signal.connect(slot); // register slot
signal(); // emit → calls all connected slots| Technique | Use Case | Pros | Cons |
|---|---|---|---|
| Callback Function | Simple notifications, C-style APIs | Fast, low overhead | Unsafe void*, hard with objects |
| Observer Pattern | OOP callback design | Strongly typed, safe | Slightly more boilerplate |
| Signals/Slots (Notifications) | Many listeners, large systems | Very flexible, decoupled | Extra library or framework needed |
// autotimer.h
#include <string>
class AutoTimer
{
public:
explicit AutoTimer(const std::string &name);
~AutoTimer();
private:
class Impl; //内部类声明
Impl *mImpl;
};
// ------------------------
class AutoTimer::Impl
{
public:
/// Return how long the object has been alive
double GetElapsed() const
{
#ifdef _WIN32
return (GetTickCount() - mStartTime) / 1e3;
#else
struct timeval end_time;
gettimeofday(&end_time, nullptr);
double t1 = mStartTime.tv_usec / 1e6 + mStartTime.tv_sec;
double t2 = end_time.tv_usec / 1e6 + end_time.tv_sec;
return t2 - t1;
#endif
}
std::string mName;
#ifdef _WIN32
DWORD mStartTime;
#else
struct timeval mStartTime;
#endif
};
AutoTimer::AutoTimer(const std::string &name) :
mImpl(new AutoTimer::Impl())
{
mImpl->mName = name;
#ifdef _WIN32
mImpl->mStartTime = GetTickCount();
#else
gettimeofday(&mImpl->mStartTime, nullptr);
#endif
}
AutoTimer::~AutoTimer()
{
std::cout << mImpl->mName << ": took " << mImpl->GetElapsed()
<< " secs" << std::endl;
delete mImpl;
}TIP: When using the pimpl idiom, prefer to use a private nested implementation class. Only use a public nested Impl class (or a public nonnested class) if other classes or free functions in the .cpp must access Impl members.
Another design question worth considering is how much logic to locate in the Impl class. Some options include:
1. Only private member variables,
2. Private member variables and methods, or recommend
3. All methods of the public class, such that the public methods are simply thin wrappers on top of equivalent methods in the Impl class.
A Cþþ compiler will create a copy constructor and assignment operator for your class if you don’t explicitly define them.
so, 得处理Impl的指针的问题:
- Make your class uncopyable. 使用std::unique_ptr
- Explicitly define the copy semantics.
- 值语义:create a copy of the Impl object instead of just copying the pointer
- 引用语义:智能指针
One of the inconvenient and error-prone aspects of pimpl is the need to allocate and deallocate the implementation object.
TIP: Think about the copy semantics of your pimpl classes, and favor the use of a shared or unique pointer to manage initialization and destruction of the implementation pointer.
TIP: Declare the constructor, destructor, copy constructor, and assignment operator to be private (or protected) to enforce the Singleton property. Or delete the copy constructor and assignment operator with the delete specifier.
Singleton &Singleton::GetInstance()
{
static Singleton instance;
return instance;
}Under these early versions of the standard,if two threads happened to call our GetInstance() method at the same time, then the instance could be constructed twice, or it could be used by one thread before it had been fully initialized by the other thread.
This issue was addressed in the Cþþ11 standard, which states that only one thread can enter the initialization of a static and that the compiler must not introduce any deadlocks around its initialization. So the previous implementation of GetInstance() is thread-safe as of Cþþ11.
Dependency injection is a technique in which an object is passed into a class (injected), rather than having the class create and store the object itself.
Dependency injection can be viewed as a way to avoid the proliferation of singletons by encouraging interfaces that accept the single instance as an input rather than requesting it internally via a GetInstance() method.
依赖注入可以被看作是避免单例泛滥的一种方式通过鼓励接口接受单个实例作为输入,而不是通过GetInstance()方法在内部请求它。
TIP: Dependency injection reduces coupling between objects and makes it easier to support unit testing
The Monostate pattern allows multiple instances of a class to be created in which all of those instances use the same static data.
// monostate.h
class Monostate
{
public:
int GetTheAnswer() const { return sAnswer; }
private:
static int sAnswer;
};
// monostate.cpp
int Monostate::sAnswer = 42;In a recent retrospective interview, the authors of the original design patterns book stated that the only pattern they would consider removing from the original list is Singleton. This is because it’s essentially a way to store global data and tends to be an indicator of poor design
TIP: There are several alternatives to the Singleton pattern, including dependency injection,the Monostate pattern, and the use of a session context