C++ Primer易忘点备录

使用预处理器进行调试

C++有时也会会有条件的执行某些用于调试的代码。
这种想法是:程序所包含的调试代码仅在开发过程中执行,当应用程序完成的时候,并且 准备提交的时候,就会将调试代码关闭。 ## 可以使用NDEBUG预处理变量实现有条件的调试代码 ## 例:

1
2
3
4
5
6
7
int main()
{
#ifndef NDEBUG
cerr<<"start debug"<<endl;
#endif
return 0;
}

当然,NDEBUG这个预处理器变量呢,当你提交Release版本的程序代码时,这个变量就会自动定义, 自然, #if#endif 之间的代码只能在 Debug版本中运行啦

预处理器还定义了其余四种在调试是非常有用的常量
__FILE__ 文件名
__LINE__当前的行号
__TIME__文件被编译的时间
__DATE__文件被编译的日期

使用范例

1
2
3
4
5
6
7
if(word.size() < 2)
cerr<<"Error: "<<__FILE__
<<" :line "<<__LINE__<<endl
<<" Compiled on"<<__DATE__
<<" at "<<__TIME__<<endl
<<" word read was "<<word
<<": legth too short"<<endl;

另一个常见的调试技术是使用NDEBUG中的assert(断言)预处理宏

#include <cassert>

使用格式:

1
assert(expr)

只要NDEBUG未定义 ,assert宏就求解条件表达式expr,如果结果是falseassert输出信息并终止程序的执行,如果输出一个非0值(true) ,则assert不做任何操作

与异常不同,程序员使用assert来检查“不可能发生的错误”
为什么说是“不可能发生的错误”?
答:就是不怕万一,只怕一万嘛,你想想,一旦检查出错误来,程序就会被强行终止,什么情况下会需要这样嘞?
当然是 当程序犯下了不可能发生的错误了 ,要不然就像发生异常,谁会蛋疼到把程序直接给挂了呀……

容器适配器

这个容器适配器啊,很容易让人忽视的,因为很少人用……但是!我觉得比较有用,所以放这了 本质上呢,适配器就是是一个事物的行为类似于另一个事物行为的一种机制。容器适配器让一种已存在的容器类型采用 另一种不同的抽象类型的工作方式实现。

标准库提供三种容器适配器 queue , priority_queue , stack

1
2
3
4
5
6
7
8
======================适配器通用的操作和类型=================================
size_type 一种类型,足以存储此适配器类型的最大对象的长度
value_type 元素类型
container_type 基础容器的类型,适配器在此容器类型上实现
A a; 创建一个新的空适配器
A a(c); 初始化为 容器c 的副本
关系操作符
=========================================================================

头文件

1
2
#include <queue>
#include <stack>

适配器初始化

示例:

1
2
deque<int> deq;
stack<int> stk(deq);

覆盖基础容器类型

默认的stackqueue都是基于deque容器实现 , 而priority_queue则在vector容器上实现。
在创建适配器时 , 通过将一个顺序容器指定为适配器的第二个类型实参,可覆盖其关联的基础容器类型
例如:

1
2
3
stack<string , vector<string> > str_stk;
stack<string , vector<string> > str_stk2(svec);
//默认情况下,栈适配器建立在deque容器上,因此采用deque提供的操作实现栈功能,但是这样做的话,以后stack的所有操作将建立在vector上了

说明:
对于给定的适配器 ,其关联的容器必须满足一定的约束条件。
- stack适配器所关联的基础容器可以使任意类型,因此,stack可以建立在vector deque , list之上
- queue适配器要求所关联的容器支持push_front运算,因此只能建立在list之上,而不能使用vector
- priority_queue适配器要求可以随机访问数据,因此,只可以建立在vectordeque之上

适配器的关系运算

两个相同类型的适配器可以做相等。不等。小于。大于。小于等于。大于等于关系比较, 只需要其关联的基础容器支持小于和等于操作即可

这里,特别地介绍下栈适配器

1
2
3
4
5
6
7
===============================栈容器适配器支持的操作======================================
s.empty() 如果栈为空,返回true
s,size() 返回栈中的元素个数
s.pop() 删除栈顶元素,但不返回其值
s.push(item) 在栈顶压入新元素
s.top() 返回栈顶元素的值,但不删除
===========================================================

下面的程序使用这五个栈操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const stack<int>::size_type stk_size = 10;
stack<int> inStack;
int ix = 0;
while(inStack.size() != stk_size)
inStack.push(ix++);
int error_cnt = 0;
while(inStack.empty() == false)
{
int value = inStack.top();
if(value != --ix)
{
cerr<<"oops! expected "<<ix
<<" received "<<value <<endl;
error_cnt++;
}
inStack.pop();
}
cout<<"our progra ran with"<<error_cnt<<" error!"<<endl;
//所有容器适配器都根据其基础容器类型所支持的操作来定义自己的操作

默认情况下,栈适配器建立在deque容器上,因此采用deque提供的操作实现栈功能
例如: inStack.push(ix++)

这个操作通过调用push_back操作实现,而push_backstack所基于的deque对象提供。 但是,尽管栈是以deque容器为基础实现的,但不能直接访问deque所提供的操作

再谈谈队列和优先级队列

队列:FIFO

优先级队列:允许用户为队列中存储的元素设置优先级。这种队列不是直接将元素放置到对尾,而是放在 比它优先级低的元素前面。标准库默认使用元素类型的< 操作符来确定他们之间的优先级关系

1
2
3
4
5
6
7
8
9
10
=================================队列和优先级队列支持的操作================================
q.empty()
q.size()
q.pop() 删除队首元素,不返回其值
q.front() 返回队首元素 //该操作只适用于队列
q.back() 返回对尾元素的值 //只适用于队列
q.top() 返回具有最高优先级的元素值//只适用于优先级队列
q.push(item) 对于queue,在对尾压入一个元素
对priority_queue , 在基于优先级的适当位置插入元素
=======================================================================

管理指针成员

设计具有指针成员的类时,类的设计者不许首先需要决定的是该指针应提供什么行为。

将一个指针复制到另一个指针时,两个指针指向同一个对象。此时,一个指针的改变会 引起基础对象的改变。
然而,可以通过复制控制策略,可以为指针成员实现不同的行为。大多数C++类采用以下三种方法之一来管理指针:

  • 指针成员采用常规指针行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。(破罐破摔型)
  • 类可以实现所谓的“智能指针”行为 , 指针所指向的对象是共享的,但类能够防止悬垂指针(智能型)
  • 类采取值型行为。指针所指向的对象是惟一的,由每个类对象独立管理(值型)
    > 为了管理具有指针成员的类,必须定义三个复制控制成员:复制构造函数、赋值操作符、析构函数 > ,这些成员可以定义指针成员的指针行为或值型行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
~~~~~~~~~~提供一个类,下面会用到的,先不要管啦~~~~~~~~~~~~~~~~~~~
class HasPtr
{
public:
HasPtr(int *p , int i):ptr(p) , val(i){}
int *get_ptr() const
{ return ptr;}
int get_int() const
{ return val;}
void set_ptr(int *p)
{ ptr = p;}
void set_int(int i)
{ val = i;}
int get_ptr_val() const
{ return *ptr;}
void set_ptr_val( int i) const
{ return *ptr = i;}
private:
int *ptr;
int val;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

定义智能指针类

智能指针除了增加功能外,其行为像普通指针一样

引入使用计数

这是定义智能指针的一个通用手段。智能指针类将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一个指针,当计数为0时,删除对象。

原理:

  • 每次创建类的新对象时 ,初始化指针并将使用计数置为1
  • 当对象作为另一个对象的副本而创建时,复制构造函数复制指针并增加与之相对应的使用计数的值
  • 当一个对象进行赋值时,赋值操作符减少左操作数所指的对象的使用计数的值(如果使用计数减至0,则删除对象)并增加右操作数所指对象的使用计数值
  • 最后调用析构函数时,析构函数减少使用计数的值,如果计数值为0,则删除基础对象

实现使用计数的两种经典使用模式

一、单独定义一个具体类用以封装使用计数和相关指针。

1
2
3
4
5
6
7
8
9
10
11
12
class U_Ptr
{
private://当然,可以省略的
friend class HasPtr;
int *ip;
size_t use ;
U_Ptr(int *p):ip(p), use(1){}
~U_Ptr()
{
delete ip;
}
};

工作原理:
U_Ptr类保存指针和使用计数,每个HasPtr对象将指向一个U_Ptr对象,使用计数将跟踪指向每个U_Ptr对象的HasPtr 对象的数目

假定刚从指向int值42的指针创建了一个HasPtr对象,则如图:

int *p = new int(42)                         |int
     ^-------------------------------------->|42  存储区
                                     ^    
HasPtr ogj(p , 10)                  U_Ptr---------^
    ^----------------------------^

如果要复制这个对象

int *p = new int(42)                        |int
    ^-------------------------------------->|42  存储区
                                       ^    
HasPtr ogj(p , 10)                    U_Ptr---------^               HasPtr copy(ogj)
    ^----------------------------^-----------------------------------^

使用计数类的使用

新的HasPtr类保存一个指向U_Ptr对象的指针,U_Ptr对象指向实际的int基础对象。必须改变每个成员以说明HasPtr类指向一个U_Ptr对象而不是一个int

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class HasPtr
{
public:
HasPtr(int *p , int i)::ptr(new U_Ptr(p)) , val(i){}
HasPtr(const HasPtr &org):ptr(org.ptr) , val(org.val)
{
++ptr->use;
}
HasPtr& operator=(const HasPtr&);
~HasPtr()
{
if(--ptr->use ==0)
delete ptr;
}
private:
U_Ptr *ptr;
int val;
};

赋值和使用计数

1
2
3
4
5
6
7
8
9
10
HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
++rhs.use;
if(--ptr.use == 0)
delete ptr;
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
//这个操作符在减少左操作数的使用计数之前使rhs的使用计数加1,从而防止自身赋值

改变其他成员

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
class HasPtr
{
public:
int *get_ptr() const
{
return ptr->ip;
}
int get_int() const
{
return val;
}
void set_ptr(int *p)
{
ptr->ip = p;
}
void set_int(int i) const
{
val = i;
}
int get_ptr_val() const
{
return *(ptr->ip);
}
int set_ptr_val(int i) const
{
*(ptr->ip) = i;
}
private:
U_Ptr *ptr;
int val;
};

还有一种模式为句柄类

C++中用句柄类来存储和管理基类指针。

C++面向对象编程的一个颇具讽刺意味的地方是:不能使用对象支持面向对象编程,相反,必须使用指针或引用

指针所指的对象类型可以变化,它既可以指向基类对象,又可以指向派生类对象,用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此句柄的用户可以获取动态行为但无需操心指针的管理

包含了继承层次的句柄有两个重要的设计考虑因素:

  • 像对任何保存指针的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针或像一个值
  • 句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象

两种不同的句柄

  • 指针型句柄 首先说明本节用到的类的关系:
    Sales_item是我们即将要定义的指针句柄类,表示Item_base层次, 而Item_base是一个基类,Bulk_item公有继承Item_base
    Sales_item的用户将像使用指针一样使用它:用户将Sales_item绑定到Item_base类型的对象并 使用*-> 操作符执行Item_base的操作。

例如:

1
2
Sales_item item(Bulk_item("0-201-82470-1", 35 , 2 , .20));
item -> net_price(); //virtual call to net_price function

定义句柄

Sales_item句柄类的使用计数策略

                Item_base item
                 ^        ^
Sales_item obj(item)-----    |       |       ---------Sales_item copy(obj)
                                    use(2)

Sales_item类将由两个数据成员,都是指针:一个指针将指向Item_base对象,而另一个将指向使用计数。Item_base指针可以指向Item_base对象也可以指向Item_base派生类对象。通过指向使用计数,多个Sales_item共享同一个计数器

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
class Sales_item
{
public
Sales_item():p(0) , use(new std::size_t(1)){}
Sales_item(const Item_base&);
Sales_item(const Sales_item&):
p(i.p) , ues(i.use){ ++*use;}
~Sales_item()
{ decr_use();}
Sales_item& operator=(const Sales_item&);
const Item_base *operator->() const
{
if(p)
return p;
else
throw std::logic_error("unbound Sales_item");
}
const Item_base& operator*() const //为什么这两个操作符只定义const版本呢?因为基类Item_base层次中的成员都是const成员
{
if(p)
return *p;
else
throw std::logic_error("unbound Sales_item");
}
private:
Item_base *p;
std::size_t *use;
void decr_use()
{
if(--*use == 0)
{
delete use;
delete p;
}
}
};

重载赋值操作符

1
2
3
4
5
6
7
8
Sales_item& Sales_item::operator=(const Sales_item& rhs)
{
++*rhs.use;
decr_use();
p = rhs.p;
use = rhs.use;
return *this;
}
  • 复制未知类型
    要实现接受Item_base对象的构造函数,必须首先解决一个问题:我们不知道给予构造函数的对象的实际类型。我们知道它是一个Item_base对象或者是一个Item_base派生类型对象。句柄类需要在不知道对象的确切类型时分配已知对象的新副本
    要怎么解决这个问题嘞?
    答:定义虚操作进行复制,我们将其命名为clone
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Item_base
{
public :
virtual Item_base* clone() const
{
return new Item_base(*this);
}
};
class Bulk_item : public Item_base
{
public:
Bulk_item* clone() const
{
return new Bulk_item(*this);
}
};

很明显,这两个函数的返回类型不一样,不是说对于虚函数,派生类型必须与基类类型实例的返回类型完全匹配吗?

但是有一个例外:如果虚函数的基类实例返回类型为引用或指针,则虚函数的派生类实例可以返回 基类实例返回的 类型的派生类(或者是类类型的指针或引用)

定义句柄构造函数:

1
2
Sales_item::Sales_item(const Item_base& item):
p(item.clone()) , use(new std::size_t(1)){}

定义完以后当然得说说怎么用啦 (1) 比较两个Sales_item对象

1
2
3
4
inline bool compare(const Sales_item& lhs , const Sales_item& rhs)
{
rturn lhs->book() <rhs->book();//item_base类里提供了虚函数book()
}
  1. 使用带比较器的关联容器 通过将比较器存储在容器对象中,可以保证比较元素的每个操作都一致地进行
1
typedef bool (*comp)(const Sales_item& , const Sales_item&);

使用compare定义一个空的multiset//这个关联容器的用法见:http://bbs.fishc.com/home.php?mod=space&uid=209696&do=blog&id=1526

1
2
3
4
std::multiset<Sales_item , comp> items(compare);
//这个定义是说,items是一个multiset,它保存Sales_item对象并使用comp对象比较它们。multiset是空的
//我们没有提供任何元素,但我们确定提供了名为compare的比较函数,当在items中增加或查找元素时,将用compare
//函数进行排序

==定义一个Basket的类,练习容器和句柄类=====

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Basket
{
typedef bool (*comp)(const Sales_item& , const Sales_item);
public:
typedef std::multiset<Sales_item , comp> set_type;
typedef set_type::size_type size_type;
typedef set_type::const_iterator const_iter;
Basket():items(compare){}
void add_item(const Sales_item& item)//接受Sales_item对象引用并将该项目的副本放入multiset
{ items.insert(item);}
size_type size(const Sales_item &i) const//返回购物篮中该ISBN的记录次数
{ return items.count(i);}
double total() const;
private:
std::multiset<Sales_item , comp> items;
}

使用句柄执行虚函数
定义total函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double Basket::total() const
{
double sum = 0.0;
for(const_iter iter = items.begin() ;
iter != items.end() ; ++iter)
{
sum +=(*iter)->net_price(items.count(*iter));
//一次性将同名书籍全部处理
}
return sum;
}
um +=(*iter)->net_price(items.count(*iter));
/*对 iter 解引用获得基础 Sales_item 对象,对该对象应用 Sales_item 类
重载的箭头操作符,该操作符返回句柄所关联的基础 Item_base 对象,用该
Item_base 对象调用 net_price 函数,传递具有相同 isbn 的图书的 count 作
为实参。net_price 是虚函数,所以调用的定价函数的版本取决于基础
Item_base 对象的类型。*/

定义值型类

具有值语义的类所定义的对象,其行为很像算术类型的对象:复制值型对象时,会得到一个不同的新副本
对副本所做的改变不会反映在原有对象上,反之亦然

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
//要使得指针成员表现得像一个值,复制HasPtr对象时必须复制指针所指的对象
class HasPtr
{
public:
HasPtr(const int&p , int i):ptr(new int(p)) , val(i){}
HasPtr(const HasPtr& org):ptr(new int(*org.ptr) , val(org.val)){}
HasPtr& operator=(const HasPtr&);
~HasPtr()
{
delete ptr;
}
int get_ptr_val() const
{
return *ptr;
}
int get_int() const
{
return val;
}
void set_ptr(int *p)
{
ptr = p;
}
void set_int(int i)
{
val = i;
}
int *get_ptr() const
{ return ptr;}
void set_ptr_val(int p) const
{ *ptr = p; }
private:
int *ptr;
int val;
};
//复制构造函数不再复制指针,它分配一个新的int对象,并初始化该对象以保存与被复制对象相同的值
//每个对象都保存属于自己的int值的不同副本,因此,析构函数无条件删除指针
赋值操作符重载
HasPtr& HasPtr::operator =(const HasPtr &rhs)
{
*ptr = *rhs.ptr;
val = rhs.val;
return *this;
}

转换与类类型

转换操作符

该种操作符是一种特殊的类成员函数。它定义将类类型转变为其他类型值的转换。转换操作符在 类定义体内声明,在保留字operator之后跟着转换的目标类型

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SmallInt
{
public:
SmallInt(int i = 0):val(0)
{
if(i <0 || i> 255)
throw std::out_of_range("bad SmallInt initializer")
}
operator int() const //转换函数不应该改变被转换的对象,因此,转换操作符通常定义为const成员
{
return val;
}
private:
std::size_t val;
};

定义形式:

1
operator type();

这里,type表示内置类型名。类类型名或有类型别名所定义的名字。

对任何可作为函数返回类型的类型(除了 void)都可以定义转换函数。

一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数类型)以及引用类型是可以的

//虽然转换函数不能有返回类型,但每个转换函数必须显式返回一个指定类型的值

注意:只能应用一个类类型转换

类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换。则代码将出错

隐式类类型转换

即其他类型转换为类类型的方法

可以用单个实参来调用的构造函数定义了从形参类型到该类型的一个隐式转换
例:

1
2
3
4
5
6
class Sales_item
{
public:
Sales_item(const string& = " "):isbn(book), units_sold(0) , revenue(0.0){}
Sales_item(std::istream &is);
};

这里每个构造函数都定义了一个隐式转换。

使用:

1
2
3
string null_book = "9-9999-9999-88889";
item.same_isbn(null_book);
item.same_isbn(cin); //注意,same_isbn成员原先的形参是 const Sales_item&

抑制由构造函数定义的隐式转换:
很简单,就是在你定义的构造函数之前加上 explict 这个关键字来限制

为转换而显式的使用构造函数

1
2
string null_book = "9-9999-9999-88889";
item.same_isbn(Sales_item(null_book));

温馨提示:通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit,这样可以 避免错误,当转换有用时,用户可以显式的构造对象

热评文章