C++与STL笔记

本文概述了EffectiveC++中的关键概念,如const成员函数的使用、构造函数优化、智能指针的正确操作、值与引用传递、避免遮掩继承名称、虚拟函数参数与效率、STL容器的最佳实践以及排序策略。这些技巧旨在提高代码效率和可维护性。
摘要由CSDN通过智能技术生成

Effective C++笔记

1.bitwise constness与logical constness

bitwise constness的主张是在const成员函数内,只有在这个成员函数不更改对象的任何成员变量(static)除外时在可以说是const。这正是c++对constness的定义,也是编译器的实现。

logical constness的主张是一个const成员函数可以修改它所处理对象内的某些成员变量,但只有在客户端侦测不出的情况才行。如果想要实现logical constness,可以使用mutable关键字声明成员变量,这样就可以释放掉non-static成员变量的witwise constness拘束,即在const成员函数内修改成员变量而编译器不报错。

另一种特殊情况,在类设计时同时为"const成员函数且返回值为const的引用类型" 与 "non-const成员函数且返回值为non-const的引用类型" 声明函数实现时,如果两个函数的仅有上述两点不同而代码相同,我们不应该分别实现这两个函数,即实现两次。为了避免其中的代码重复以及维护,修改时的巨大工作量,我们真正该做的是仅实现其中某一个函数,并使其中一个调用另一个,这促使我们将常量性移除(casting away constness)。在这个过程中,应使const成员函数实现出其non-const孪生兄弟,在non-const成员函数中调用const成员函数的方法类似以下语句:

return const_cast<char&>(static_cast<const Block&>(*this)[position]);

具体请见条款03:尽可能使用const

补充:const成员函数可以访问类的所有成员,并可以修改static类型的成员变量。

2.在类构造函数中使用成员列表初始化代替赋值动作

原因: c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。

若采用在构造函数内为成员变量赋值,则实际运行时函数将首先调用成员变量的默认构造函数为其赋值,然后再立即对它们赋予新值。若采用成员列表初始化,则在初值列中针对各个成员变量而设的实参则被拿去作为各成员变量的构造函数的实参。在大型类的设计当中,赋值动作带来的内存损耗与效率问题将非常明显,

具体请见条款04:确定对象被使用前已先被初始化

补充:常量与引用类型的成员变量的构造必须在成员列表初始化中实现。

3.若不想使用编译器自动生成的成员函数,应明确拒绝

原因:c++中,编译器可以暗自为类创建默认构造函数、复制构造函数、拷贝构造函数、拷贝构造操作符等。如果不想要错误的调用不需要的自动创建的函数,则需明确拒绝。

一个做法是在类中将其声明为private成员函数,且不需要定义其实现,这样就可以阻止编译器暗自创建其专属版本,并且由于其为private函数,若客户企图调用该函数则会在编译器报错,从而将连接期错误转移至编译期。

另一种做法是实现一个base class,在让我们的类继承该类。注意,这项技术可能会导致多重继承,具体请见条款06:若不想使用编译器自动生成的成员函数,就该明确拒绝

 class Uncopyable
  {
  protected:
    Uncopyable() {};
    ~Uncopyable() {};
  private:
    Uncopyable(const Uncopyable&); //阻止copy
    Uncopyable& operator=(const Uncopyalbe&);  //阻止
  };

4.以独立语句将new生成的对象存储入智能指针

原因:c++编译器在编译函数时,不像java和c#那样总是以特定次序完成函数参数的核算,也就是说当函数有多个参数时,其将会随机执行。

例如在在调用函数时写成如下所示,该语句虽然非常简单,仅有一行完成,但其背后蕴含着更为严重的问题。

processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());//错误!
//编译器可能的执行顺序:
//new Widget
//priority(),×这里如果对priority的调用导致异常,则new Widget返回的指针将会丢失
//tr1::shared_ptr构造函数

在这种执行顺序下,new Widget返回的指针没有进入shared_ptr内,而后者是我们用来防止资源泄露的武器,最终将会导致new Widget的资源没有通过shared_ptr得到释放,有可能会引发资源泄露

解决方法也很简单,只要使用分离语句,也就是写两行就可以避免这类问题,写法如下:

std::tr1::shared_ptr<Widget>pw(new Widget);
processWidget(pw,priority());//这个动作绝对不会导致资源泄露

具体请见条款17:以独立语句将newed对象置入智能指针

5.函数传参时用常量引用传递取代值传递

老生常谈的话题了。当使用值传递时会导致对象的构造函数与析构函数被多次调用造成性能损耗。但是在常量引用传递时真正传递的是指针,就会提升性能。而且使用引用传递可以避免派生类传给基类参数时导致的切割问题(slicing problem)。另外,该规则并不适用于内置类型,以及STL的迭代器和函数对象,对它们而言使用值传递往往比较适当。

具体请见条款20:宁以pass-by-reference-to-const替换pass-by-value

6.尽量晚声明变量以提高效率

由条款4可知"通过default构造函数构造出一个对象然后对它赋值"的效率比"直接在构造时指定初值"差,因此要尽可能的延后变量的声明时间。在代码块的开始就声明变量不如在需要时声明并构造变量。

另一方面,在循环中对变量的声明有两种方式,区别在于变量定义在循环外还是循环内:

方式A:定义在循环外
Widght w;
for(int i=0;i<n;++i){
    w=某个值;
    ...
}
方式B:定义在循环内
for(int i=0;i<n;++i){
    Widght w(某个值);
    ...
}

哪一个比较好?这里分析一下两种写法的成本:

  • 做法A:1个构造函数+1个析构函数+n个赋值操作
  • 做法B:n个构造函数+n个析构函数

选择哪一个做法的原则:

  1. 判断赋值成本和构造+析构成本的大小
  2. 判断变量w作用域是要在外面还是要在里面
  3. 是否正在处理代码中效率高度敏感的部分

具体请见条款26:尽可能延后变量定义式的出现时间

7.在继承中要避免遮掩父类的成员函数名

原因:编译器在作用域内发现了某变量x,他将会先在该作用域内查找该变量x。如果该作用域没有,则上升到上一个大的作用域,最后上升到global作用域去找。

在类中,如果子类声明了和父类同名的函数,将会遮掩父类函数,使子类在调用函数时只能调用子类中的函数而不是父类中的函数(虽然两者同名,但编译器会先在子类中找到函数定义并选中该函数执行,而不是“聪明的”跑到父类作用域中去调用相关的函数)。无论base类和derived类内的函数有不同的参数类型,无论函数是虚函数还是非虚函数,该规则都适用。

如果非要调用,可以使用using声明式来解决这个问题,如

class Derived:public Base{
public:
    using Base::f1;//让base类中的f1在derived中可见(并且public)
    vitual void f1();
}

这样就不会违反public继承中的基类与派生类之间的is-a关系了。

如果不想继承基类的所有函数,在private继承下,这样的实现可能是有意义的。这样就没法使用using(using使父类的该函数在子类中都可见)。可以使用转交函数的方法,即在某个函数中调用父类函数,如

class  Derived:private Base{
    virtual void mf1(){Base::mf1();}//调用父类的函数
};

具体请见条款33:避免遮掩继承而来的名称

8.virtual函数为动态绑定,但其参数却为静态绑定

举例:在父类中将虚函数的默认参数设定为blue,在子类中将继承而来的虚函数的默认参数设定为red。此时再声明一个基类指针指向子类对象,通过该指针调用虚函数且不传参数,那么实际上调用的则是子类的虚函数加上父类的虚函数的默认参数blue而不是red。这种两者各出一半力的情况来源于c++的指导原则。

如果缺省参数值是动态绑定而不是静态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。为了程序的执行效率和编译器上实现的简易度,c++做了这样的取舍。

可以考虑使用virtual函数的替代设计,可以看条款35。如NVI(non-virtual interface),令公有非虚函数调用虚函数。

具体请见条款37:绝不重新定义继承而来的缺省参数值


Effective STL笔记

1.区间成员函数优先于单元素成员函数

什么意思呢,就是在容器中的一个位置构造出一个范围内的元素,例如使矢量v1的内容与矢量v2的内容后半部分相同,尽量使用

v1.assign(v2.begin()+v2.size()/2,_2.end())

,如果使用insert在循环语句中不断插入,首先可能使迭代器指向不明导致出错,其次相比区间成员函数将会更多地调用内存分配与构造函数是一笔性能损耗,甚至可能导致vector数组的多次重新销毁和分配新的内存空间。

使用区间成员函数还有几个好处,第一它写起来很容易,可以减少代码量,第二可以得到意图清晰且更加直接的代码,第三c++标准要求如insert的区间成员函数直接把元素移动到他们的最终位置上,可以提高效率。

使用区间成员函数,常见有以下四种方法:

  • 区间构造:如vector<int>v2(v1.begin(),v1.end());
  • 区间插入:如v1.insert(v1.begin(),v2.begin(),v2m.end());(在v1.begin处插入v2的begin到end区间)
  • 区间删除:如v1.erase(v1.begin(),v1.end());
  • 区间赋值:如v1.assign(v2.begin(),v2.end());

2.容器中的对象总是深复制副本

在STL容器中,当插入元素时,通常会执行深复制,因为容器需要存储元素的副本,而不是元素本身。这意味着在向容器中插入大型对象或者很多对象将会需要更多的内存和时间。而且在继承关系下,复制动作将会导致剥离(slicing) ,在派生类对象被复制进基类对象容器时,它特有的部分(即派生类中的信息)将会丢失。

3.C++中总是经可能地把代码解释为函数声明

有一些复杂,具体来说就是在函数参数中,以下三种形式都将被当做函数指针作为参数

  • int g(double(*pf)());        //g指向函数的指针为参数
    • int g(double pf());        //同上,pf为隐式指针
      • int g(double ());        //同上,省去参数名

因此在特定情况下将会导致编译器"错误地"在调用函数传参时,将一些类型当成函数类型,举例如下

ifstream dataFile("ints.dat");
list<int>data(istream_iterator<int>(dataFile),istream_iterator<int>());
//第二个参数的类型是指向不带参数的函数的指针,错误!!!

正确做法是在data的声明中避免使用匿名迭代器(尽管使用匿名对象是一种趋势),拆开来写就没问题。

具体请见第六条:当心C++编译器最烦人的分析机制

4.使用"swap"技巧来除去多余的容量

如代码所示,缩减vector矢量实际占用的内存大小。将size为10,但capacity为10000的数组以一种优雅的方式变成size和capacity都为是10的数组。

vector<int>(a).swap(a);

#include <iostream>
#include <vector>
using namespace std;
int main() {
    vector<int>a(10000,1);
    cout << "初始的空间大小:" << a.capacity() << ",实际的数组容量:" << a.size() << endl;
//10000,10000
    a.erase(a.begin() + 10, a.end());
    cout <<"交换前的空间大小:"<< a.capacity() <<",实际的数组容量:"<<a.size() << endl;
//10000,10
    vector<int>(a).swap(a);
    cout << "交换后的空间大小:" << a.capacity() << ",实际的数组容量:" << a.size() << endl;
//10,10
    return 0;
}

补充:在swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。

具体请见第17条:使用"swap技巧"除去多余的容量

5.在关联容器中的等价和相等的区别

如果下述的表达式的结果为真,则说明两个值中的任何一个(按照一定的排序准则)都不在另一个的前面,那么这两个值(按照这一准则)就是等价的。

!(w1<w2)&&!(w2<w1)

标准关联容器总是要保持排列顺序,所以每个容器必须有一个比较函数(重载operator<,默认为less)来决定保持怎样的顺序。而等价的定义正是通过该比较函数而确定的,因此,标准关联容器的使用者要为所使用的每个容器指定一个比较函数(用来决定如何排序)。

具体请见第19条:理解相等(equality)和等价(equivalence)的区别

6.注意map[]和map.insert的效率问题

原因:map[k]=v的原理是先查找k是否已经在map中,若不存在则以v作为值插入。如果已经存在了,则更新原来的值为v。如果值类型为类类型,当使用 map[k] 访问一个不存在的键 k 时,会调用该类类型的默认构造函数来创建一个新的值,并将其插入到映射容器中,然后返回对新值的引用。这个效率肯定不会比RAII原则的效率高,即构造时直接设定好初值。

insert的原理是在插入时,若原来的元素已存在则插入失败,返回false。若不存在则直接插入。注意。若不存在则insert相比operator[]节省了三个函数调用,分别是创建临时对象的默认构造函数,析构该临时对象的析构函数,以及调用其赋值操作符。

也就是说,在map[k]失败时,则会调用insert进行插入操作,但是这里会相比insert多了几个函数调用。如果map中存储的是类类型,而类的构造,析构,赋值操作的代价非常昂贵,在这里就不如使用insert进行操作。

结论:

  • 当向映射表中添加元素时,优先使用insert。尽管operator[]也能实现添加元素的效果。
  • 当更新已经在映射表中存在的元素的值时,优先选择operator[]。

注意我们可以自己实现一个函数来兼具两种操作的优点。

具体请见第24条:当效率至关重要时,请在map::operator[]与map::insert之间做出谨慎选择

7.使用distance和advance来转换const迭代器

首先:强制类型转换无法将const迭代器转换为非const迭代器,根本原因是对于这些容器类型,iterator和const_itreator是完全不同的类,而试图将一种类型转换为另一种类型是毫无意义的。无论const_cast, reinterpret_cast都不能完成这种操作。虽然vector和string容器是个例,但是这样会导致可移植性问题,这里不在说明。

不过,为了获得指向const迭代器所指位置的非const迭代器,我们还有一种“曲线救国”的方案。具体做法如下所示:

vector<int>container;//某个容器
vector<int>::const_iterator cit=container.begin();
//假设该const迭代器指向容器中的某个位置

vector<int>::iterator it=container.begin();
advance(it,distance<vector<int>::const_iterator>(it,cit));
//完成!

注意:因为distance需要两种相同类型的参数来调用,如果不明确声明调用的是<const_iterator>版本的爹迭代器,则p会出现指向不明确的错误,即编译器无法为你实现。所以我们需要明确我们调用的是const迭代器版本,并且由于it能够转换为常量迭代器,该函数得以正确运行并返回it与cit之间的距离。此时在让it前进这个距离,就可以使it和cit指向同一个元素,完成曲线救国。

至于效率问题,对于随机访问迭代器,如vector,string和deque是常数时间的操作,其他容器将会是线性时间的操作。不过如26条所言,在使用容器时,尽量使用iterator代替const和reserve类型的迭代器。

具体请见第27条:使用distance和advance将容器的const_iterator转换成iterator

8.为你的排序作出最佳选择

排序选择依据:

  1. 如果执行完全排序,则使用sortstable_sort。
  2. 如果需要对等价性最前面的n个元素排序,也就是选出vector,string,deque或者数组中的最小的n个元素进行排序,使用partial_sort。
  3. 如果需要找到排序后的第n个元素,而又不需要对这n个元素进行排序,则使用nth_element。
  4. 如果要将容器内的元素按照某个特定的条件分开,选择partitionstable_partition。
  5. 如果容器为list,仍可以直接调用partitionstable_partiton ( 该算法本质为双指针,无需随机访问迭代器 ) 。其他请用list::sort来替代sort和stable_sort算法。

排序消耗资源顺序(由小到大):

  1. partition
  2. stable_partition
  3. nth_element
  4. partial_sort
  5. sort
  6. stable_sort

其他:nth_element这个算法除了找到排名在前n个元素之外还有找到某个特定百分比上的元素的功能。例如查找位于排序后中间位置的元素可以使用迭代器指向容器中中间位置的元素。如:auto it=container.begin()+container.size()/2;这样就可以通过it来访问排序后的中间位置元素。要注意nth_element是质变算法,会改变原数组。

具体请见第31条:了解各种与排序有关的选择

  • 42
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值