你可能不知道的 C++(二)

第一部分:你可能不知道的 C++(一)


此为《你可能不知道的 C++》的第二部分,讨论 类型内联 & 模板

类型

我们不讨论原始类型(int, double, etc.)和结构。原始类型比较简单,而结构其实就是类。

本节包含以下几个方面:

  • 联合

  • 指针和引用

  • 常量

  • 转型

联合

联合(union)的元素共用同一块内存空间,联合的大小就是最大元素的大小。联合里所存对象的类型,编译器是不知道的,所以虽然它可以有成员函数,也没有多大意义。

用联合来统一接口

在 Win32 里,特定于消息(message)的数据通过两个 32 位的参数来传递:WPARAMLPARAM

LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

用户得记住每一种消息所对应的 WPARAMLPARAM 的含义,这样非常不便。下面是 Win32 消息处理函数的典型写法:

{
  switch (msg) {
  case WM_USER:
    if (lParam == WM_RBUTTONUP) { /*...*/ }
      break;

  case WM_COMMAND:
    switch (LOWORD(wParam)) {
    case IDM_EXIT:
      SendMessage(hwnd, WM_CLOSE, 0, 0);
      break;
    case IDM_CREATE:
      create();
      break;
    //...
    }
  // ...
  }
}

而 Xlib 通过联合,既保证了类型安全,又方便了使用。每一种事件(event,等价于 Win32 下的消息),都封装在一个结构中,比如XKeyEventXButtonEvent,等等,它们各有其自身特定的字段,但是开头几个字段是公共的,叫XAnyEvent。Xlib 把这些结构统一在联合XEvent中:

typedef union _XEvent {
  XAnyEvent xany; // 公共字段
  XKeyEvent xkey;
  XButtonEvent xbutton;
  XMotionEvent xmotion;
  ...
} XEvent;

第一个公共字段为type,记录事件类型:

struct XAnyEvent {
  int type;
  // ...
}

接下来,事件处理的代码清晰易懂:

{
  switch (evt.xany.type) {
  case <按钮事件>:
    // 通过 evt.xbutton 访问按钮事件的特定字段
    break;

  case <键盘事件>:
    // 通过 evt.xkey 访问键盘事件的特定字段
    break;

  // ...
  }
}

两相比较,高下自见分晓。

用联合测试大小端

联合最著名最巧妙的应用就是测试大端(big endian)小端(little endian),Linux 内核就是这么做的:

static union { char c[4]; unsigned long mylong; } endian_test = { { 'l', '?', '?', 'b' } };
#define ENDIANNESS ((char)endian_test.mylong)

用法:

if (ENDIANNESS == 'l') /* little endian */
if (ENDIANNESS == 'b') /* big endian */

访问控制

第一点:访问控制作用于类而非对象。

怎么理解?看一个例子。

class foo {
public:
  int bar(foo* f) {
    return a + f->a;
  }

private:
  int a;
};

虽然 a 是私有成员,但在成员函数 bar() 里依然可以访问 f->a

反过来考虑,如果访问控制作用于对象级别,那么一切都得是public才行,否则像拷贝构造函数这样的操作就没办法实现了。

留一个问题,下面的代码可以通过编译吗?

class B {
protected:
    int a;
};

class D : public B {
public:
    int f(B* b) {
        return a + b->a; // ?
    }
};

第二点:访问控制作用于编译时而非运行时。

还是来看一个例子。

class Node {
public:
  Node() : value(0) {
  }

  int getValue() const {
    return value;
  }

private:
  int value;
};

虽然成员变量value是私有的,且没有提供setValue()方法,但是只要“知道”它的地址,仍然可以改变它:

Node node;
*reinterpret_cast<int*>(&node) = 1;  // value被改成了1

我们断定value的地址就是Node对象的地址,如果Node有虚函数或基类,value的地址就难说了。

成员函数指针

成员函数指针可能不单单是一个指针(指向成员函数代码的起始地址),它可能是一个小型的结构,编码了很多额外的信息,比如函数是否虚拟(virtual)、是否多继承,等等。

成员函数指针自身不可提领(dereferenced),必须借助具体的对象才能调用。

假设有一个图形库,基类为Graphic

class Graphic {
public:
  virtual void draw() {}
};

成员函数draw()的指针记为:

void (Graphic::*draw)() = &Graphic::draw;

注意取址符 & 是不能省略的,这一点跟正常的函数指针不太一样。

可以用typedef让类型更清晰一些:

typedef void (Graphic::*draw_t)();
draw_t draw = &Graphic::draw;

要提领成员函数指针,必须借助一个对象,这个对象也就充当了this指针的作用。

Graphic g;  // 定义一个对象
(g.*draw)();  // 在这个对象上调用函数指针

如果成员函数没有访问对象的成员变量,甚至可以通过NULL来提领。

((Graphic*)NULL)->*draw();

但是这种情况非常少见,也没什么意义。不过 Linux 内核的链表就是基于这一点来实现的,宏list_entry根据结点的字段反推结点的地址:

#define list_entry(ptr, type, member)\
  ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

其中,&((type *)0)->member即是在空对象上访问字段member,再取址从而得到member字段相对于结构的地址偏移。

扯远了,回归正题。提领成员函数指针的语法还是挺让人困惑的,可以定义一个宏来辅助:

#define CALL_MEMBER_FN(obj, mfp) ((obj).*(mfp))

CALL_MEMBER_FN(g, draw);

成员函数指针的大小跟一般指针也不一样。再看几个图形库的类:

// 直线
class Line : public Graphic {
public:
  virtual void draw() {}
};

class Listener {
};

// 垂线
class OrthoLine : public Graphic, public Listener {
public:
  virtual void draw() {}
};

在 GCC 和 MSVC 上的测试结果(32位):

GCCMSVC
sizeof (&Graphic::draw)84
sizeof (&Line::draw)84
sizeof (&OrthoLine::draw)88

在类的继承结构中使用成员函数指针,不可避免的需要转型,直接使用static_cast即可。

从子类转到基类:

typedef void (Graphic::*graphic_draw_t)();
graphic_draw_t draw = static_cast<graphic_draw_t>(&Line::draw);

从基类转到子类:

typedef void (Line::*line_draw_t)();
line_draw_t draw = static_cast<line_draw_t>(&Graphic::draw);

在界面框架里,比如 wxWidgets 和 MFC,成员函数指针一般指向事件(消息)处理函数,存放在消息映射表里。

指针和引用

先来读个程序吧,能猜到输出吗?
(1987 年第四届 IOCCC,最佳单行程序,David Korn)

main() { printf(&unix["\021%six\012\0"],(unix)["have"]+"fun"-0x60);}

猜不到?
好吧,给点提示,下面这些表达式结果都一样:

x[3]  *(x+3)  *(3+x)  3[x]

还是猜不到?好吧,更多提示:http://www.ioccc.org/1987/kor...

指针的加减

给定类型T的指针(T不为void):

T* p;

p + 1就等价于:

static_cast<char*>(p) + sizeof(T)
static_cast<uintptr_t>(p) + sizeof(T)
static_cast<size_t>(p) + sizeof(T)

void*做加减没有任何意义,且不能编译:

void* p;
// ...
++p; // 不能编译!
p = p + 1;  // 不能编译!

两个指针相减,所得差值可以保存在ptrdiff_tintptr_t类型的变量中:

int *p1, *p2;
ptrdiff_t d = p2 - p1; // or intptr_t d = ...

ptrdiff_tintptr_t这两个类型与size_t类似,在表达指针差值方面,既正确又可移植。

最后,给定T的两个变量:

T a, b;

可以得出结论:

(&a - &b) * sizeof(T) == (ptrdiff_t)((uintptr_t)&a - (uintptr_t)&b)
引用

这段代码能编译吗?

void pump1(int& i) { }
void pump2(const int& i) { }

long lval = 24;
pump1(lval); // ?
pump2(lval); // ?

这是 Stanley Lippman 在某次演讲中用过的例子,在此就不详细解释了,动手试试就知道。

C++ 为什么要引入引用?
其实大多数人都没想过这个问题,就算想了,也摸不清重点。

我们来看一个例子。
给定一个表示矩阵的类Matrix,现在想实现 + 操作符,如果没有引用,那只能这样:

Matrix operator+(Matrix m1, Matrix m2);

我们都知道这会有性能问题,因为参数是“传值”的,每次调用都会有临时对象的构造和销毁。对一个3D程序来说,这种矩阵运算每秒钟可能有上万次,这种不必要的浪费会导致严重的性能问题。

参数使用指针,可以避免性能问题:

Matrix operator+(Matrix* m1, Matrix* m2);

但是指针的语法并不直观(non-intuitive,指针本质上就是一个间接层),也容易出错。

不直观这一点是无法接受的,因为操作符重载背后的思想,就是为了让类对象的使用变得直观。

此外,指针的语法,也不能阻止程序员写出这样的代码:

Matrix d = &a + &b + &c; // Oops!

这种连加操作根本不能编译,&a + &b返回的是值,无法再与&c相加,如果改成返回指针,又有内存管理的问题。

Matrix* operator+(Matrix* m1, Matrix* m2);

正因为此,C++ 之父才决定引入引用。引用解决了对象语法的性能短板,在保留指针的高效、避免对象拷贝开销的同时,提供了对象操作的直观的语法。

常量

const was a useful alternative to macros for representing constants only if global consts were implicitly local to their compilation unit. Only in that case could the compiler easily deduce that their value really didn't change.

(Bjarne Stroustrup,《C++ 的设计和演化》, 3.8)

这里的常量,特指以const关键字修饰的变量。

两个编译单元里的常量,即使包含自同一个头文件,也互不干扰。

// const.h

const int INT = 1;
// test_1.cc

#include "const.h"
const void* get_int_address_1() { return &INT; }
// test_2.cc

#include "const.h"
const void* get_int_address_2() { return &INT; }
// main.cc

extern const void* get_int_address_1();
extern const void* get_int_address_2();

int main() {
  // 地址不一样,说明各有一份
  assert(get_int_address_1() != get_int_address_2());
}

编译单元test_1.cctest_2.cc都包含了const.h里的常量INT,但其实它们各有一份自己的INT,互不干扰。

如果头文件里定义的常量不是intdouble这种原始类型,必须加上关键字static,否则多个编译单元在链接(link)时,会报错说有重复定义。

// const.h

static const std::string STR = "test";

转型

C++ 提供了四个转型操作符。

static_cast中的static是指编译时,转型失败的话就不能编译。
dynamic_cast中的dynamic是指运行时,转型失败的话会返回NULL(转指针时)或引发std::bad_cast异常(转引用时)。
reinterpret_cast意为重新解释,转型时不做任何检查。
const_cast只是去掉常量性。

下面说说常用的其它两种转型,取自 Google Chrome 的源码。

implicit_cast
template<typename To, typename From>
inline To implicit_cast(From const &f) {
  return f;
}

用法:

double d = 3.14;
int i = 3;
std::min(d, implicit_cast<double>(i));

你可能会有两个问题:

  • 能不能之间用std::min(d, i)?

  • 为什么不用static_cast?

答案

  • implicit_cast只在特殊情况下才有必要,即当一个表达式的类型必须被精确控制时,比如说,为了避免重载(overload)。

  • implicit_cast相较于其它转型的好处是,读代码的人可以立即就明白,这只是一个简单的隐式转换,不是一个潜在的危险的转型(不完全正确,见下文)。

如下代码:

int t = d; // 警告:可能有数据损失
std::min(t, i);

就等价于:

std::min(implicit_cast<int>(d), i); // 警告:可能有数据损失

可见,implicit_cast简化了隐式转型的用法。

直接用static_cast或 C 风格强转是很危险的,因为编译器不再警告。

std::min(static_cast<int>(d), i); // 没有警告
std::min((int)d, i); // 没有警告

再看一个例子:枚举间的转型。

enum E1 { e1_0, e1_1, e1_2 };
enum E2 { e2_0, e2_1, e2_2 };

E1 e = static_cast<E1>(e2_0);

有些编译器不喜欢枚举到枚举的转型,因此我们先implicit_castint

E1 e = static_cast<E1>(implicit_cast<int>(e2_0));
down_cast

down_cast是在继承结构中往下转型,这也正是down的含义,它是用来替代dynamic_cast的,没有运行时检查,直接用static_cast来做转型,从而提高性能。当然,使用场景也就受了限制,只有当你 100% 确定 FromTo 的关系时,才能使用,否则后果自负。

template<typename To, typename From>
inline To down_cast(From* f) {
  if (false) {
    implicit_cast<From*, To>(0);
  }

#if !defined(NDEBUG) && !defined(GOOGLE_PROTOBUF_NO_RTTI)
  assert(f == NULL || dynamic_cast<To>(f) != NULL);  // RTTI: debug mode only!
#endif
  return static_cast<To>(f);
}

down_cast的实现巧妙的使用了implicit_cast,让编译器帮助做了类型检查,而 if (false) 条件保证了最终肯定会被编译器优化掉,所以对性能没有任何影响。

内联 & 模板

一些原则:

  • 如果一个编译单元使用了某个内联(inline),那么它就必须要能够看到这个内联的定义。

  • 模板在实例化(instantiation)时必须要能够看到这个模板的定义。

  • 如果一个编译单元使用了某个模板的(完整)特例化,那么它不必看到这个特例化的定义,只要看到声明就可以了,一如非模板的情况。(如果特例化是在头文件中,它便总是内联的。)

模板 vs. 宏

应该尽可能避免使用宏。怎么做?用模板和内联,模板提供泛型(genericity),内联提供效率(efficiency)。

举个例子,宏:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

可以替换为:

template <typename T>
inline T min(const T& a, const T& b) {
  return a < b ? a : b;
}

如果你需要拼接符号(tokens)的能力,那么就必须用宏,无可替代:

比如用宏来初始化一个结构体:

struct image_info {
  std::string file;     // 图片的文件名
  unsigned char* img2c; // 缺省使用的图片(一块内存)
  size_t img2c_len;     // img2c数组的大小
};

#define _PNG(name) #name ".png", name##_png, sizeof(name##_png)

还有那个 Linux 内核链表的例子,除了宏也不可能:

#define list_entry(ptr, type, member) \
    ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

模板特例化

模板特例化(template specialization)可以理解成一种静态派(static dispath)发机制。

注意: 本文不会讨论模板偏特化(partial specialization)。

函数的特例化

STL 里的模板函数swap针对vector做了特例化,直接调用vector自己的swap成员函数,从而避免了拷贝和复制。

template <class T>
void swap(T& a, T& b) { T tmp = a; a = b; b = tmp; }

template <class T>
void swap<vector<T> >(vector<T>& a, vector<T>& b) { a.swap(b); }
模板特例化 & 模板重载

模板特例化和模板重载(template overloading)是两回事。

template <class Base, class Exponent>
Base pow(Base b, Exponent e);

template <>
int pow(int b, int e); // 特例化

template <class Base>
Base pow(Base b, int); // 重载
类的特例化

举一个不太好的例子,vector针对bool的特例化,达到节省内存的目的:一个bool只用一个 bit。

template <typename T>
class vector {
  T* vec_data;
  // ...
};

template <>
class vector<bool> {
  unsigned int *vector_data;
  // ...
};

关于 vector<bool> 的问题,值得另写一篇文章了。

Type traits

详见 Boost 库的头文件:boost/type_traits/remove_pointer.hpp

template<typename T> struct remove_pointer {
  typedef T type;
};
template<typename T> struct remove_pointer<T*> {
  typedef T type;
};
template<typename T> struct remove_pointer<T* const> {
  typedef T type;
};

用法:

remove_pointer<int>::type i = 1;
remove_pointer<int*>::type j = 2;

Google Chromium IPC uses template specialization to define how a data type is read, written and logged in the IPC system.

template <class P> struct ParamTraits {
};
template <>
struct ParamTraits<bool> {
  typedef bool param_type;
  static void Write(Message* m, const param_type& p);
  static bool Read(const Message* m, void** iter, param_type* r);
  static void Log(const param_type& p, std::string* l);
};
template <>
struct ParamTraits<int> {
  typedef int param_type;
  static void Write(Message* m, const param_type& p);
  static bool Read(const Message* m, void** iter, param_type* r);
  static void Log(const param_type& p, std::string* l);
};
template <>
struct ParamTraits<std::string> {
  typedef std::string param_type;
  static void Write(Message* m, const param_type& p);
  static bool Read(const Message* m, void** iter, param_type* r);
  static void Log(const param_type& p, std::string* l);
};

全文完

第一部分:你可能不知道的 C++(一)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是将图片数据从内存中拷贝到本地文件的C++代码示例: ```c++ #include <Windows.h> #include <fstream> void save_image_to_file(const void* image_data, size_t image_size, const char* file_name) { // 创建本地文件 HANDLE hFile = CreateFileA(file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { // 创建文件失败 return; } // 写入图片数据 DWORD bytes_written = 0; if (!WriteFile(hFile, image_data, static_cast<DWORD>(image_size), &bytes_written, NULL)) { // 写入数据失败 CloseHandle(hFile); return; } // 关闭文件句柄 CloseHandle(hFile); } int main() { // 假设图片数据在内存中的地址为image_data,长度为image_size const void* image_data = (void*)0x12345678; size_t image_size = 1024; // 将图片数据写入本地文件 save_image_to_file(image_data, image_size, "test.jpg"); return 0; } ``` 在这个示例中,我们首先使用 `CreateFileA` 创建本地文件,并指定了 `GENERIC_WRITE` 权限以便写入文件。如果创建文件失败,函数会直接返回。接下来,我们使用 `WriteFile` 将图片数据写入文件中,如果写入失败,函数同样会直接返回。最后,我们关闭文件句柄,释放资源。 需要注意的是,这个示例中我们使用了硬编码的图片数据地址,实际上,你需要将其替换为你自己的数据地址。另外,由于图片数据可能包含进制数据,因此我们使用了 `void*` 类型来保存图片数据的地址,而不是 `char*` 类型。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值