LucienXian's Blog


  • 首页

  • 归档

  • 标签

[Effective cpp]-#3

发表于 2017-11-20

use const whenever possible

关于const的使用,之前的文章里也做过相似的介绍,但《effective C++》里还是做了一些规则推荐,那就来学习一下。

函数声明

讲道理,之前对这种用法并不熟悉,但书上做了很详细的介绍。令函数返回一个常量值在某些情况下是非常有用的。举个例子,考虑有理数:

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

这样一个重载函数返回的是一个常量值,倘若不加上const关键字,就有可能发生这种情况:

1
2
3
4
Rational a, b, c;
(a * b) = c;//这样做毫无意义
//又或者出现这种情况
if (a*b = c)//也许只是写错

const成员函数

  • 这里需要注意的是如果两个成员函数只是不同,它们是可以被重载的;
  • 由于这个原因,重载的两个成员函数可能会出现代码重复的情况;
  • 因此我们可以这样使用:
1
2
3
4
5
6
7
8
class TextBlock{
public:
const char& operator[](std::size_t position) const{... return text[position];}
private:
char& operator[](std::size_t position){
return const_char<char&>(static_cast<const TextBlock>(*this)[position]);
}
}
  • 通过在non-const operator[]经过两次转换,即可以减少代码量,也可以返回non-const值;
    • non-const转换成const是安全转换;

建议

  • 将某些东西声明为const可以帮助编译器检查出错误的用法;
  • 当const和non-const有着类似的实现时,可以让non-const调用const来避免代码重复;

[Effective cpp]-#2

发表于 2017-11-20

prefers const, enums, and inlines to #defines

这条规则的本质是利用编译器去替换预处理器。

比如在代码中,ASPECT_RATIO可能会被编译器忽略,因为编译器在处理源码之前预处理器以及把这个标记移走,而该记号无法被记载进symbol table。所以如果遇到编译错误时,我们可能只看到1.653而无法看到ASPECT_RATIO,特别是假如改行代码是别人写的,你就更加难以发现错误了。

1
#define ASPECT_RATIO 1.653

const

  • 这时我们可以使用const取代,由于const是语言常量,因此该标记肯定会被编译器看到;
  • 另外,我们不能用#define创建一个类内常量,而const可以;
1
2
3
4
class MyClass{
private:
static const int Num = 5;//也可以只是声明const int Num; 在另一个文件内定义int MyClass:Num = 5;
}

enum

  • 假如编译不允许在类内使用static整数型常量,此时可以用the enum hack的做法;
1
2
3
4
5
class MyClass{
private:
enum {Num = 5};
int scores[Num];
}
  • 只有const声明的变量不可以取地址,而enum可以取地址;

inline

  • 这是为了取代宏定义一个函数的操作;
  • 例如:
1
2
3
4
#define CALL(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL(++a, b);//a累加了两次
CALL(++a, b+10);//a累加了一次
  • 由于宏定义是直接替换,所以为了避免不必要的麻烦,我们必须为每个变量添加符号;
  • 另外这里的用法还有一个大麻烦,由于替换的原因,这里a在比较时竟然与b有关;
  • 因此我们可以用模板来代替:
1
2
3
4
5
template <typaname T>
inline void call(const T& a, const T& b)
{
f(a > b ? a : b)
}

总结

  • 对于单纯变量,最好用const或者enum取代#define
  • 对于函数宏,可以用inline函数代替

[Effective cpp]-#1

发表于 2017-11-20

不管怎样,先立个flag,要每天看一章节《effective c++》

Abstract Factory(DesignPattern)

发表于 2017-11-18

Abstract Factory

目的

提供一个创建一系列相关对象的接口,无需为它们指定具体的类

动机

假设有一个支持多种风格的界面工具包,此时不应该为每一种特定风格的组件进行硬编码。因此我们可以定一个WidgetFactory的抽象类,提供返回窗口组件,门组件等接口。这样,用户程序就只用与抽象类定义的接口进行交互,而不用与具体的类进行交互。

适用范围

  • 一个系统要独立于它的产品创建、组合和创建;
  • 一个系统由多个产品系列中的一个来配置;
  • 强调一系列相关产品对象的设计以便进行联合使用;
  • 提供一个产品类库,但只想显示接口而不是实现;

效果

  • 分离了具体的类:由factory去帮助你控制一个应用创建的对象的类;
  • 易于变换产品系列:只需要变换factory即可;
  • 有利于产品的一致性:当一个应用只使用一个factory提供的产品即可实现;
  • 难以支持新种类的产品(缺点):当扩展一个factory的新产品时,此时子类也必须全部发生变化;

源代码示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Abstract_Factory.cpp : 定义控制台应用程序的入口点。

#include <iostream>

class Product {
public:
virtual ~Product() { }
virtual void produce() = 0;

};

class DerivedproductA : public Product {
public:
void produce() {
std::cout << "Product produceA..." << std::endl;
}
};

class DerivedproductB : public Product {
public:
void produce() {
std::cout << "Product produceB..." << std::endl;
}
};

class AbstractFactory
{
public:

virtual ~AbstractFactory() {}
virtual Product* createproductA() = 0;
virtual Product* createproductB() = 0;
};

class ConcreteFactory : public AbstractFactory {
public:
Product* createproductA() {
return new DerivedproductA();
}

Product* createproductB() {
return new DerivedproductB();
}
};




int main()
{
AbstractFactory *factory = new ConcreteFactory();//virtual 具有封装功能
Product* productA = factory->createproductA();
Product* productB = factory->createproductB();
productA->produce();
productB->produce();

delete factory;
delete productA;
delete productB;
return 0;
}

四种类型转换——CPP

发表于 2017-11-16

C++的四种类型转换

static_cast< T *>(e)

  • 静态转换,在编译期间进行处理;
  • 常用:
    • 在C++中内置的基本数据类型之间进行转换;
    • 在基类和子类之间的指针或引用进行转换,基类指针或引用转换成子类的是不安全的,相反则是安全的;
    • 把void类型指针转换成目标类型;(不安全)
    • 把任何类型的表达式转换成void类型;

const_cast< T*>(e)

  • 作用于同一个类型之间的去常和添常属性的转换;
  • 可以在添加常量属性或者去除常量属性;

dynamic_cast< T * >(e)

  • 在运行期进行类型转换;
  • 如果转换成功则返回指针或者引用,失败则返回NULL(指针)或者抛出异常std::cast(引用);
  • 通常情况,转换时基类要有虚函数;

reinterpret_cast< T*>(e)

  • 执行低级转换,具体结果取决于编译器;
  • 比如将一个int* 转换成int;
  • 它的原理是对二进制进行重新的解释;

注意

  • 尽量避免类型转换,在注重效率的代码中尽量避免dynamic_cast;
  • 如果类型转换时必要的,应该隐藏在某个函数中;
  • 尽量用上述四种类型转换,不要使用旧式转换;

智能指针的学习——CPP

发表于 2017-11-16

智能指针

之前的文章有说过:在C++里,动态内存的分配是通过一对运算符——new和delete完成的。但这样往往会造成内存泄漏或者是在还是指针指向内存的时候释放了内存。

因此标准库提供了两种智能指针来管理对象——shared_ptr(允许多个指针指向同一个内存)、unique_ptr(指针独占对象),另外标准库还提供了一个weak_ptr的伴随类,指向shared_ptr管理的对象。

shared_ptr

shared_ptr的目标是:在指向的对象不再被使用的时候,即自动释放相关对象的内存。

计数器

智能指针内部有一个计数器,无论何时我们拷贝一个shared_ptr,计数器都会递增,它的作用就是记录着当前有多少个指针在引用着这个对象。

当计数器为0时,智能指针会自动释放其管理的资源。

当用一个shared_ptr去初始化另一个shared_ptr,作为参数传递进函数,以及作为函数返回值,引用计数都会递增;

当给一个shared_ptr赋予新值,或者shared_ptr被销毁,如离开作用域,则引用计数递减。

销毁对象

当指向一个对象的shared_ptr被销毁时,shared_ptr类会自动递减所指向对象的引用计数,并检查是否为0,如果为0则去释放内存。

weak_ptr

weak_ptr与shared_ptr最大的不同是weak_ptr在指向同一个对象时不能递增引用计数。

它不可以控制所指向对象的生存周期,因为它指向的是一个由shared_ptr管理的对象。

  • 作用:防止shared_ptr的循环引用;
    • B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个shared_ptr,此时A和B的生命周期互相由对方决定,事实上都无法从内存中销毁。
  • 作用获得this指针的shared_ptr,使得对象自身能够生产shared_ptr来管理自己;

虚函数的学习(2)——CPP

发表于 2017-11-15

关于虚函数的学习二

纯虚函数

定义

纯虚函数是在基类中声明的基函数,它没有定义,并且要求任何派生类都必须实现自己的定义方法,否则该派生类不能实例化。

使用方法是在函数原型后面加“=0”

1
virtual void func()=0;

引入原因

  • 为了多态性,我们需要在基类中实现虚函数;
  • 某些情况下,基类不应该生成对象。例如,一个动物的基类可以派生出兔子、老虎等类,但动物基类不应该实例化。

抽象类

  • 带有纯虚函数的类为抽象类;
  • 抽象类的作用是为派生类提供一个公共的根,要求派生类实现抽象类描述的操作操作。
  • 如果派生类没有自定义抽象类中的虚函数,则该派生类仍然是一个抽象类。

虚析构函数

引入原因

当我们去delete一个动态分配的对象指针的时候将执行析构函数,该指针有可能指向继承体系中的某一个对象,出现指针的静态类型和所指对象的动态类型不符合的情况。因此,为了让编译器清楚执行哪个版本的析构函数,我们需要在基类中将析构函数定义为虚函数。

如果基类的析构函数不是虚函数,那么delete一个指向派生类对象的基类指针将会产生未定义行为。

构造函数中调用虚函数

先说结论,尽量不要这样使用

因为在构造函数中调用虚函数,会使得虚函数的机制失效。在构造函数中调用虚函数,由于构造顺序是先构造父类再构造子类,如果构造父类的时候调用了虚函数,这时查询的将是父类的虚函数表地址。然后再构造子类,此时查询的将是子类的虚函数表地址。

这是为了避免出现bug。因为如果构造父类时查询的是子类的虚函数表地址,而子类的虚函数又用到了子类自定义的数据成员,此时该数据成员还没完成初始化,这样做就会出现错误。

析构函数中调用虚函数

与上原因,同理。但析构函数的析构顺序与构造函数的构造顺序相反。当基类的析构函数执行时,派生类的析构函数已经执行完成,派生类的数据成员已经失效,如果基类还要调用派生类的虚函数,则可能会出错。

析构函数抛出异常

总结;

  • 析构函数尽量不要抛出异常;
  • 如果真的要抛,则在析构函数里面解决,但这种方法不好,因为不能及时地知道异常出现在什么地方;

因为类对象分配的资源由对象的析构函数执行,如果在释放资源之前,异常发生,那么分配的资源将无法被释放。所以我们必须要在类中控制资源的释放。

多态与虚函数——CPP

发表于 2017-11-15

多态性与虚函数

多态性

多态意义就是使得接口重用,不管传过来哪一个类的对象,我们都可以通过同一个接口去找到相对应的调用方法;

OOP的核心思想之一。当我们使用基类的引用或者指针去调用基类中定义的一个函数时,我们并不知道该函数真正的作用对象是基类的对象还是子类的对象。引用或指针的静态类型和动态类型不一致正是支持这一特性的根本原因。如果该类是一个虚函数,那么直到运行时才能完全确定执行那个版本,依据则是指针或者引用绑定了哪个版本的对象。

虚函数

通常用法

声明基类的指针,利用该指针去指向任意一个子类对象,调用相应的虚函数,这样可以根据子类的不同而调用不同的方法;

派生类中的虚函数

  • 如果在派生类中覆盖了某个虚函数,可以再一次用virtual关键字进行声明,也可以不用,因为默认如果基类的某个函数是虚函数,则所有派生类中它都是虚函数。
  • 派生类覆盖的虚函数的参数列表类型必须与基类中的函数形参类型严格匹配;

虚函数表

虚函数本质上是通过一张虚函数表实现的,每当程序声明了一个虚函数时,就会为这个类构造一张表。V表是由虚函数的地址组成的。

每当有虚函数被调用时,V表就会被用来解析函数地址。包含虚函数的类的对象实例都会包含一个虚拟指针,这个指针会在对象地址的最开始处,称为vptr,通过这个vprt就可以找到V表的内存地址。而V表又包含一个或多个虚函数的基地址。

这就可以完成虚函数调用期间的动态绑定:在运行时,首先是基类的指针被赋予派生类对象的地址,这样就可以找到所属类的虚函数表的地址,通过虚函数的名字在虚函数表中找到对应的虚函数地址,然后进行调用。

注意:虚函数表是针对类的,同一个类的多个对象都指向同一个虚函数表;

new&malloc——CPP

发表于 2017-11-15

new & malloc

分配区域

通常情况下,new和malloc都是指分配在堆上,也有说new是分配在自由存储区,而malloc则是分配在堆上,自由存储区不一定是堆。

操作系统维护着一个空闲的内存链表,当需要分配内存的时候,就会查找这个表,直到找到一块内存大于所需内存的区域,就分配内存并返回多余内存给链表;

作用机理

new

1
2
string *sp = new string("a value");//分配并初始化一个对象
string *arr = new string[10];//分配10个默认初始化的对象
  • 这里一共执行了3步操作:
    • new调用一个名为operator new/new[]的库函数,该函数分配正确的原始内存来存储对象或对象数组;
    • 编译器运行构造函数初始化对象,并传入初始值;
    • 返回指向该对象的指针;
1
2
delete sp;
delete [] arr;
  • 执行了两步:
    • 执行相应的析构函数;
    • 调用operator delete的库函数,释放内存空间;

如果应用程序希望自定义内存分配,则需要自己定义operator new和operator delete;

malloc

malloc函数是一个库函数,接受一个表示待分配的size_t字节数,返回分配空间的指针,或者NULL表示分配失败。

区别

  • mallco和free是库函数,而new和delete则是操作符;
  • new自己计算所需空间大小,malloc则需要指定大小;
  • new在分配空间之后能进行初始化,malloc只是分配空间并返回指针;

使用

  • malloc
1
2
3
4
5
6
7
8
9
int *a  = (int *)malloc ( sizeof (int ));
if(NULL == a)
{
...
}
else
{
...
}
  • new
1
2
3
4
5
6
7
8
9
int * a = new int();
if(NULL == a) //无意义,因为能走到这步已经以为着分配正常,new在分配失败的时候会抛出异常
{
...
}
else
{
...
}

如果希望new在失败时不是抛出异常,而是返回NULL,可以这样用:

1
2
3
4
5
6
int* p = new(std::nothrow) int;
//或者重载operator new和delete函数
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(size_t, nothrow_t&) noexcept;
void *operator delete[](size_t, nothrow_t&) noexcept;

const限定符——CPP

发表于 2017-11-14

const限定符

编译器会找到所有const对象,将常量值做替换。

作用域

  • 默认情况下,const只在本文件内有效,因为这样编译器在做替换时才能访问到初始值,因此每个文件都必须要有定义。而为了避免重命名,const对象只在本文件内有效;
  • 如果想要在多文件内共享变量,则用extern限定;

引用

1
2
3
4
5
6
7
8
const int a = 1024;
const int &b = a;//正确
b = 42;//错误,不能通过引用修改常量
int &c = a;//错误,不能将让非常量引用指向常量
double doub = 3.14;
const int &d = doub;//初始化时,允许将一个非常量做为初始值,此时绑定的是一个临时的常量,因此不允许非常量引用时强制转换;
//const int temp = b;
//const int &d = temp;

常量引用非常量,只是不能通过该常量去修改非常量的值;但如果有其他途径修改非常量的值也是可以的。

指针

  • 要想存放常量的地址,只能使用指向常量的指针;
  • 但指向常量的指针不一定指向常量,跟引用一样,可以通过其它方式修改指向的值;
1
2
3
4
int errNum = 0;
int *const curErr = &errNum;//常量指针,将一直指向errNum
const double pi = 3.14;
const double *const pip = &pi;//指向常量对象的常量指针

函数中使用const

函数参数

1
2
3
4
void func(const int var);//无意义,传过来的参数不可以改变,但var本身就是实参;
void func(const int* var);//参数指针所指的内容;
void func(int *const var);//无意义,传过来的指针不可变,同1
void func(const Class& Var); //引用参数在函数内不可以改变,通常情况下,这种用法避免建立副本,在提高效率的同时禁止对引用对象修改;

函数返回值

1
2
3
const int fun1();//无意义
const int *fun2() //调用时 const int *pValue = fun2();
int* const fun3() //调用时 int * const pValue = fun2();

跟参数的用法基本相同,除了重载运算符,其它情况下不建议将返回值设为const,因为这样对象就不能对成员进行赋值操作;

<i class="fa fa-angle-left"></i>1…25262728<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces