20200911 【Effective C++】 01

导读

最近做项目,深感自己C++功底不足。故开始看Effective C++。

【Effective C++】系列为笔记。

少用define

现在我们用#define,主要是两种用途:

  1. 定义常量
  2. 定义函数

对于第一种,书中推荐使用const,如果是类常量,那么使用static const。这是因为#define其实是一个预处理,在编译期间就会被替换掉,不会进入符号表。

这里需要注意:

  1. 常量字符串需要「两个const」,比如const char* const MESSAGE = "HelloWorld";,才能同时保证指针和对象本身都是常量
  2. 类常量为了保证一个类只有一个,所以需要是static的。类常量除了在类内声明外,还需要在实现文件中定义。.

而对于想用define来实现功能函数的,同样不建议。原因有是这样的假函数没有自己的定义域,从而:

  • 可以改变参数本身的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define Foo(Value)\
    return ++Value;

    void Bar()
    {
    int a = 1;
    Foo(a);
    Foo(a); // a = 3
    }
  • 如果定义了新的变量,无法在同一个定义域里调用多次该宏,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    #define Foo()\
    int a = 1

    void Bar()
    {
    Foo();
    Foo(); // compile error 定义了两个同名变量a
    }

如果我们是因为难以确定参数的类型而苟且使用泛型,完全可以用 Inline Template Function 来实现。更高更快更强,更优雅。

1
2
3
4
5
6
7
8
// 这么写的原因是Callback可能是各种各样的类型,无法确定
#define AddListener(TargetObject, Callback)\
GameManager()->GetDispatcher()->AddListener(TargetObject, Callback)

void Foo()
{
AddListener(this, &AThisObject::MyFunc);
}

换成Inline Template Function的形式:

1
2
3
4
5
6
7
8
9
10
template<class T>
FORCEINLINE void AddListener(UObject* TargetObject, T Callback)
{
GameManager()->GetDispatcher()->AddListener(TargetObject, Callback);
}

void Foo()
{
AddListener(this, &AThisObject::MyFunc);
}

update on 20200914

更新一个新鲜的使用宏的bug

先说结论: 宏的定义是全局的,即使不被include,也会生效。所以不要乱用宏,如果要写宏,那么也要写的容易辨认一些,比如全大写命名。

之前在写一个辅助添加监听的函数,那时还没有了解template编程,纠结于监听回调的类型问题,于是用了宏来实现,大概是这样的:

1
2
#define AddListener(EventId, Callback)\
XXXEventManager->GetInstance()->AddEvent(EventId, Callback);

这一切都没啥问题。

直到我后来接触了template,知道这种不明参数类型的函数应该用template function来实现更为恰当,于是设计了一个EventHelper类:

1
2
3
4
5
6
7
8
class EventHelper
{
template<class T>
inline static void AddListener(int EventId, T Callback)
{
XXXEventManager->GetInstance()->AddEvent(EventId, Callback);
}
}

用起来感觉良好,编译成功。

但是,我没有把原来的宏删除掉。

commit代码之后伙伴更新到了我的代码,然后在EventHelper::AddListener那里报了许许多多奇怪的语法错误。

问题是光看EventHelper::AddListener是完全没看出问题的。

后来一步步排查,只要把我原来定义的AddListener宏删掉,就能编译通过了。

得到结论是在编译的时候,编译器把EventHelper::AddListener中的AddListener字样替换成宏的内容了,所以会直接报错。

所以我们后面用宏的时候要注意:

  1. 尽量不要用宏
  2. 用了宏,名字要有辨识性,比如MACRO_ADD_LISTENER()
  3. 注意宏和define都是全局的,不要乱写

template

说到模板方法,这里提一点新手容易犯的错。

我们平时都是将函数的声明放在.h文件,将实现放在.cpp文件,用的时候include.h文件。

对于template函数来说,不能在.cpp文件中实现,否则链接的时候会出现unresolved external symbol。找问题的时候简直都怀疑自己的眼睛了,明明有定义,但是确说找不到符号。

Google之,关键字「c++ template function unresolved external symbal」。查到StackOverflow的帖子

这里面说:模板类和函数在被用到之前是不会被实例化的。当模板被用到的时候,编译器需要模板的完整定义(而非只是声明)来检查正确的类型。

所以, 模板方法的定义需要写在头文件中,并且使用的时候需要include这个头文件

多用const

指向常量的指针 vs 常指针

指向常量的指针:指向的对象是常量,不可改变。const在星号的前面,如const AActor* MyActor;

常指针:指针本身是常量,不可以指向别的对象。const在星号的后面,如AActor* const MyActor;。举例:

  • 用于STL容器返回的迭代器,返回一个T* const指针,不可以指向别的对象;

常指针常量:指针和对象都是常量,需要两个const,如const char* const MESSAGE = "HELLO WORLD";

用于修饰函数返回值,避免函数返回值被修改

例子:

1
2
3
class Rational {...}
Rational::Rational(const Rational& Another);
const Rational operator* (const Rational &lhs, const Rational &rhs);

设想如果返回的不是一个常量,那么我们就可以对其作出修改

1
2
Rational a, b, c;
(a * b) = c;

而如果返回的是一个const值,就可以避免这种情况。

Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//