关闭

C++基础和STL,Effective C++笔记

标签: C++基础C++ STLEffective C++
4076人阅读 评论(0) 收藏 举报
分类:

C++基础

static

static变量存储在静态数据区
相对于function:在函数内,变量,内存只被分配一次,多次调用值相同
相对于其他模块(.c文件):变量和函数,不能被模块外其他函数访问(private)
相对于类:类中的static变量和函数属于整个类,而不是对象

全局变量 VS 全局静态变量
若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。(private)

new,delete,malloc,free

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存
new和delete对应,new调用构造函数,delete会调用析构函数。
new在实现上调用了malloc函数。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。new delete在实现上其实调用了malloc,free函数。

delete VS delete []
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数

malloc,calloc,realloc,free
四个函数都被包含在stdlib.h函数库内。回值都是请求系统分配的地址,如果请求失败就返回NULL

void* realloc(void* ptr, unsigned newsize); 

realloc是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度。并把原来大小内存空间中的内容复制到newsize中。realloc 不能保证重新分配后的内存空间和原来的内存空间指在同一内存地址, 它返回的指针很可能指向一个新的地址。

void* malloc(unsigned size);` 

在内存的动态存储区中分配一块长度为”size”字节的连续区域,返回该区域的首地址

void* calloc(size_t nelem, size_t elsize);` 

calloc调用形式为(类型*)calloc(n,size):在内存的动态存储区中分配n块长度为”size”字节的连续区域,返回首地址。

free(q); 

其中q为已经分配的块;

引用

引用就是某个目标变量的别名(alias),对应用的操作与对变量直接操作效果完全相同。在内存中并没有产生实参的副本使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元(值传递),且需要重复使用”*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;

引用VS指针
1) 引用必须被初始化,指针不必。
2) 引用初始化以后不能被改变指向的对象,指针可以改变所指的对象。
3) 不存在指向空值的引用,但是存在指向空值的指针。

备注:引用时C++的特性,C中没有,引用是通过指针来实现的,对传递做了一些限制,但是对程序员是透明的。引用的目的,将 & 从调用者移到了被调用者处。

结构体

结构体:struct ,是由一系列相同类型或不同类型的数据构成数据的集合,也叫结构。其最主要的作用就是封装。
联合体:union,几个不同的变量存放在同一块内存区域中。也就是使用覆盖技术,几个变量互相覆盖。同一时刻只能使用一种变量。

struct VS class
默认的继承访问权限,struct是public的,class是private的。其他没有区别。

数据对齐

cosnt

  1. 定义常量:const修饰的类型为TYPE的变量value是不可变的。必须初始化
    const TYPE ValueName = value;

  2. 指针使用CONST
    (1)在*之前:指针所指向的内容是常量不可变

    const (char) *pContent;
    (char) const *pContent;
    

    (2)在*之后:指针本身是常量不可变

    (char*) const pContent; 
    
  3. 函数相关使用CONST
    (1)const修饰函数参数:修饰的参数在函数内不可以改变

    void function(const int Var);
    

    (2)const 修饰函数返回值:返回值是一个常量

    const int fun1()
    int const fun1()
    

    (3)const修饰成员函数:函数不能修改任何成员变量,不能调用非const成员函数

    class A
        { 
            …
           void function()const; 
    //1. 常成员函数, 它不改变对象的成员变量(除了static的),也不能调用类中任何非const成员函数。
    //2. const类对象只能调用其const成员函数
    //3. 对于在类外定义的成员函数,必须在成员函数的定义和声明中都指定关键字const,不然将被视为重载
    }
    

const VS #define
const 常量有数据类型,编译器可以对前者进行类型安全检查
而宏常量是别名,没有数据类型,没有类型安全检查

typedef, #define
一种类型的别名,有类型检查而不只是简单的宏替换。可以做类型,数组,函数的类型替换。

define是宏,只做简单的宏替换,没有类型检查

数组与指针

数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
sizeof可以计算出数组的容量,对指针操作得到的是一个指针变量的字节数。
当数组名作为参数传入时,实际上数组就退化成指针了。

隐式类型转换

隐式类型转换分三种,即算术转换、赋值转换和输出转换。

  • 算术转换
    1、为防止精度损失,如果必要的话,类型总是被提升为较宽的类型。
    2、所有含有小于整形的有序类型的算术表达式在计算之前其类型都会被转换成整形。
  • 赋值转换
    进行赋值操作时,赋值运算符右边的数据类型必须转换成赋值号左边的类型,若右边的数据类型的长度大于左边,则要进行截断或舍入操作。
  • 输出转换
    要输出的数据类型与输出格式不符时,便自动进行类型转换

sizeof与strlen

sizeof是算符(类似宏定义的特殊关键字),strlen是函数。
sizeof操作符的结果类型是size_t,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。它的功能是:获得保证能容纳实现所建立的最大对象的字节大小
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
strlen的功能是:返回字符串的长度。该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。

多态,虚表

多态是怎么实现的?
多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数
每个有虚函数的类(不是对象)都有一个虚表,父类的虚表,子类的虚表,虚表里面存储的是一个个指向函数的指针,父类的虚表存储父类的虚函数,在子类中,如果没有覆盖(override)父类的虚函数,虚表不变,如果覆盖了,那虚表更新,指向当前类中的函数。
每个含有虚函数的对象都有一个虚函数指针,这个指针指向该类的虚表
过程:当通过父类指针找到对象的时候,(判断)如果是虚函数,则是通过(子类的,通过this指针指向虚指针)虚指针找到虚表(子类虚表),再通过虚表对应项找到函数地址入口,这就是动态绑定,而不是在编译时候就决定了入口地址的静态绑定!
虚函数存在哪?
虚函数表vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata),微软的编译器将虚函数表存放在常量段

静态联编VS动态联编
联编是指一个计算机程序的不同部分彼此关联的过程。决定程序中谁调用谁,在运行的过程决定。如多态。
静态联编:是指联编工作在编译阶段完成的,在程序运行之前完成的,运行的时候就已经确定了。

C++程序内存分布

一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放 。
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区—存放函数体的二进制代码。

├———————┤低地址
│ 代码区
├———————┤
│ 文字常量区
├———————┤
│ 全局数据区 │
├———————┤
│ 堆 │
├———————┤
│ 栈 │
├———————┤高地址

堆从低地址向高地址扩展
栈从高地址向低地址扩展

C++变量初始化规则

  • 内置类型

全局变量————>全局区
有初始值,data段
无初始值,bss段,不占用内存空间,在运行的时候被编译器自动初始化为0

局部变量(函数内)——->栈区
无初始值,随机

总结:
放在静态数据区的(全局的)变量编译器会初始化,在栈里面的(局部)变量不会初始化。

  • 自建类型struct class
    A a;放在栈,全局区里
    A *a=new A();放在堆里

总结:

  1. new出来的变量放在堆里,直接声明(如A a)根据声明位置放在全局区,栈里
  2. 放在静态数据区的(全局的)变量编译器会初始化,在栈里面的(局部)变量不会初始化

ptr++

如果是int *ptrptr++等价于ptr+1*sizeof(int);
所以每次加1都能够指向相邻的下一个元素。
sizeof(ptr)也就是指针本身的大小永远都是4字节,所以ptr是一个4字节的值,这个值就是指向的内容的地址
ptr指的是字节的序号(但是)
ptr2-ptr1会被翻译成(ptr2-ptr1)/sizeof(int)这对程序员是透明的,便于操作数组

内联函数

内联函数,是向编译器发出的请求,编译阶段把函数内容替换到代码中去,有类型检查
宏定义,编译预处理阶段,简单的替代,没有类型检查
内联函数:本质是用程序代码的空间换取程序调用的时间,适合在一个大项目里,有一个小函数(简单几行,没有循环语句,循环语句使得调用开销相对变小)不断被重复调用
内联函数使用:声明(不需要),定义(inline)

C++中一个class类或者对象占用多少内字节

非静态成员变量总合。
加上编译器为了CPU计算,作出的数据对齐处理。
加上为了支持虚函数,产生的额外负担。

  1. sizeof(class)和他声明的对象obj,sizeof(obj)是一样大的
  2. 空类sizeof(class)=1,编译器就会给空类创建一个隐含的一个字节的空间。
  3. 非静态变量+数据对齐。静态变量不存在这里。
  4. 如果有虚函数,加4字节,指向虚表的指针,虚表也不存在这里
  5. 函数,虚表,静态变量,都是属于这个类的,存在静态存储区,也不占类空间

指针

指针函数与函数指针
指针函数(函数返回指针):是指带指针的函数,本质是函数,返回值是某一类型的指针
函数指针:指向函数的指针变量

int (*f) (int x); /* 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */

指针数组,数组指针
指针数组:一个数组,里面都是指针,int *a[10]
数组指针:一个指针,指向一个数组,int (*a)[10]
[]结合的优先级高于a

指针常量与常量指针

int const *p1 = &b;//const 在前,定义为常量指针  (常量的指针)
int *const p2 = &c;//*在前,定义为指针常量   (指针指向常量)

***a

***(a+1)
**(*a+1)
*(**a+1)
***a+1

越在上面,增加的维度越高

句柄VS指针

从广义上,能够从一个数值拎起一大堆数据的东西都可以叫做句柄。Windows系统中有许多内核对象,比如打开的文件,创建的线程,程序的窗口,等等,这些对象往往很大,而且还经常变化。
那么怎么在程序间或程序内部的子过程(函数)之间传递这些数据呢?
在进程的地址空间中设一张表(句柄即编号->实际地址的映射),表里头专门保存一些编号和由这个编号对应一个地址,而由那个地址去引用实际的对象,这个编号跟那个地址在数值上没有任何规律性的联系,纯粹是个映射而已。
句柄作用:作为一个索引在一个表中查找对应的内核对象的实际地址。是指向指针的指针,但是不能对它做不安全的操作。windows用句柄操作系统资源(对象),隐藏了系统具体的信息。指针则直接记录了物理内存地址。

以对象管理资源

auto_ptr
在C++的程序中Obj *ptr=new Obj(),需要delete ptr去释放这个ptr指向的内存。C++提供一种方法,可以不用delete就可以在函数结束的时候自动释放内存。(Effective C++ 条款13,以对象管理资源)
auto_ptr是一个模板类,这个类型的对象内部的析构函数完成对堆内存的释放,所以不要对这个对象的内存进行delete了。

//demo for manager heap memory by auto_ptr.
auto_ptr<int> pInt(new int(0));
*pInt = 2;
cout<<*pInt<<endl;

//demo for manager heap memory by hand.
int *pInt2 = new int(2);
cout<<*pInt2<<endl;
delete pInt2

其实auto_ptr是一个类,包装指针,并且重载了反引用(dereference)运算符operator *和成员选择运算符operator ->,以模仿指针的行为。

shared_ptr
的作用有如同指针,但会记录有多少个shared_ptrs共同指向一个对象。这便是所谓的引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。这在非环形数据结构中防止资源泄露很有帮助。
auto_ptr没有考虑引用计数,因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。

this指针

  1. this不占class或者对象的空间
  2. this在成员函数调用前构造,在成员函数结束时消失。
  3. this的作用是在调用成员函数的时候把对象传进去
A a;
a.foo(10);
===被编译器翻译为===>
A::foo(&a,10)
那么foo的原型
foo(int p)
===被编译器翻译为===>
foo(A * const thisint p)
所以在成员函数定义的时候可以写,this->pram=XXX;

C++程序编译过程

  1. 预处理器cpp
    预处理器cpp将对源文件中的宏进行展开,#define,#include
  2. 编译器gcc/g++
    编译器将文件编译成汇编文件
  3. 汇编器as
    汇编器将汇编文件编译成机器码
  4. 链接器ld
    将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

size_t

  1. 为了使自己的程序有很好的移植性,c++程序员应该尽量使用size_t和size_type而不是int, unsigned
  2. size_t是全局定义的类型;size_type是STL类中定义的类型属性,用以保存任意string和vector类对象的长度
  3. size_t类型是通过typedef定义的一些无符号整型的别名,通常是unsigned int或unsigned long,甚至是unsigned long long。每种标准C的实现应该选择足够大的无符号整型,来代表目标平台可能的最大对象,但不能供过于求。
    如果传入-1会变成4294967295,因为使用无符号整数表示

C++ 运算符重载

运算符重载的实质是函数重载。在实现过程中,首先把指定的运算表达式转化为对运算符函数的调用
在编译过程中:根据实参的类型把表达式转化为运算符函数,运算对象转化为运算符函数的实参
分为两种:

  1. 运算符重载为成员函数(一般说来,单目运算符(+=)最好被重载为成员)
    complex operator+(const complex &c);
    
  2. 友元函数形式,C++ 中的string就是这样(一般说来,对双目运算符(+,-,*,/)最好被重载为友元函数)
    friend complex operator +(const complex &c1, const complex &c2);
    
    +,-,*,/返回对象,=返回引用,返回值主要考虑的是连续运算
    friend ostream & operator<<(ostream & os,const MyString &st);
    
    (单目与双目,是指操作数的数目)

友元
友元定义在类外,友元可以访问类中的私有成员
好处,提高运行效率(在类外调用开销小),坏处,破坏封装性

参数处理顺序

  1. Cout是对<<运算符的重载,返回的是cout这个ostream对象本身。<<连续使用时,返回的cout对象继续调用上面函数对下一个操作数进行输出,如此反复,直到该语句结束。因此,cout执行顺序是从左到右。
  2. 编译器对函数实参的表达式处理顺序是从右向左。如
    void fun(int a, int b)
    
    先处理b后处理a

#include

C++中#include包含头文件带 .h 和不带 .h 的区别?
带 .h 的头文件是旧标准(c)的,如果想用新的标准(C++)的头文件就不要带 .h
C++增加了名称空间概念,借以将原来声明在全局空间下的标识符声明在了namespace std下。
为了和C语言兼容,C++标准化过程中,原有C语言头文件标准化后,头文件名前带个c字母,如cstdio、cstring、ctime、ctype等等。

#include <stdio.h> --> #include <cstdio> 
#include <stdlib.h> --> #include <cstdlib> 
#include <string.h> --> #include <cstring>

多重继承,虚继承

多重继承
是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性。

虚拟继承
是多重继承中特有的概念,是为解决多重继承的。在多重继承下,一个基类可以在派生层次中出现多次。virtual表明一种愿望,即在后序的派生类当中共享虚基类的同一份实例。
实例:istream,ostream,iostream
构造函数初始化方式
顺序和普通多重继承稍有区别,先初始化虚基类,然后依次初始化继承于虚基类的子类。

private protect public的可见性

访问:
(函数+友元函数)——子类函数———类对象
private: 只能由该类中的函数、其友元函数访问,不能被子类访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
继承:(父类对子类的可见性由父类内访问说明符有关,继承方式影响的是子类生成的对象(或子类的子类)对于父类成员的可见性)
使用private继承,父类的所有方法在子类中变为private;
使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
使用public继承,父类中的方法属性不发生改变,但是毕竟是两个类,不能访问private成员

抽象类,纯虚函数

定义了纯虚函数的类成为抽象类,抽象类不能实例化。

virtual void fun()=0;

C++类型转换

隐式类型转换
算数转换,复制转换,传参数

显示类型转换,static_cast

static_cast < type-id > ( expression ) 

作用1:编译器在编译期处理,在基类和派生类之间转换时使用,子类的类型提升到父类的类型
作用2:static_cast(expression) 用来强制类型转换,例如:

static_cast<int>(2.1)

显示类型转换,dynamic_cast

dynamic_cast < type-id > ( expression )  RTTI的功能
dynamic_cast<T>(expression) 主要用来进行安全向下转型

例如:只有基类可以使用,但是想调用子类的函数。如果可以,尝试使用多态来代替这种方法。

namepsace

指标识符的可见范围,C++标准库中的标识符都被定义到了std的namespace中
推荐用法

using std::cin

<iostream.h>是旧标准,标识符是全局的
<iosream>是遵循C++标准的,需要指定命名空间才能使用

C++默认产生的成员函数

构造函数,析构函数,拷贝构造函数,赋值函数
构造函数:创建一个对象时,系统自动调用构造函数。
析构函数:在函数体内定义的对象,当函数执行结束时,该对象所在类的析构函数会被自动调用
拷贝构造函数:拷贝构造函数中只有一个参数,这个参数是对某个同类对象的引用。在三种情况下被调用:

  1. 用类的一个已知的对象去初始化该类的另一个对象时。(初始化时用”Object A=B”,也可以用A(B)的形式。)
  2. 函数的形参是类的对象,调用函数进行形参和实参的结合时。(定义一个函数A,A函数的形参是类的对象,在另外一个函数B中调用这个函数A,实参将具体的对象传递给形参,这时候会调用拷贝构造函数。)
  3. 函数的返回值是类的对象,函数执行完返回调用者。(定义一个函数A,该函数的返回值是一个类的对象,在函数B中定义类的对象来接受函数A的返回,这个时候会调用拷贝构造函数。)

赋值函数:赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员(赋值构造函数被赋值的对象必须已经在内存中,否则调用的将是拷贝构造函数)

深拷贝与浅拷贝
所谓浅拷贝,指的是在对象复制时,只是对对象中的数据成员进行简单的赋值,上面的例子都是属于浅拷贝的情况,默认拷贝构造函数执行的也是浅拷贝。
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而是重新动态分配空间。需要重写拷贝构造函数来实现。

形参与实参

实参:调用时传递给函数的参数
形参:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数.在调用函数时,实参将赋值给形参。

小问题

  • C++是不是类型安全的?
    不是。两个不同类型的指针之间可以强制转换
  • main 函数执行以前,还会执行什么代码?
    全局对象(全局变量,全局静态变量)的构造函数会在main 函数之前执行。
  • assert
    assert宏的原型定义在中,其作用是如果它的条件返回错误,则终止程序执行
  • #include<a.h>#include"a.h" 有什么区别?
    对于#include ,编译器从标准库路径开始搜索
    a.h对于#include “a.h” ,编译器从用户的工作路径开始搜索 a.h(自己的)
  • 构造函数和析构函数能否声明为虚函数?
    构造函数不能声明为虚函数,析构函数可以声明为虚函数,而且有时是必须声明为虚函数。
    构造函数的调用时从上到下的,如果在构造函数中调用或者构造函数本身就是虚函数那么,子类的对象还没有构造完成就调用会出错析构函数不一样,析构函数常常需要通过多态来调用合适的函数来析构指针指向的对象。

C++ STL

STL是什么

定义:标准模板库(Standard Template Library,STL)
组成:容器(基于模板)+算法。STL是一些“容器”的集合,这些“容器”有list,vector,set,map等,STL也是算法和其他一些组件的集合。
优点:跨平台,可重用,高性能,免费。STL被内建在大部分编译器中,也可以说STL就是C++的一部分,使用起来很方便。
泛型编程:一个程序可以看做是输入,操作,输出,当不确定输入和输出的类型,但是对于数据操作的原则是一样的时候,可以使用泛型(模板机制)代替,这样可以更加抽象地表达程序,STL就是一个大量使用了泛型编程的例子。
模板机制:模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。
STL API参考

vector

实现原理
vector是在堆内存中连续存储的动态可伸缩数组。

说明
stl array固定大小,stl vector是动态可伸缩的,当前内存不够用时,内存翻倍。

常用接口

std::vector<int> second (4,100);
vector::operator[]
vector::push_back() 
vector::pop_back()
vector::clear() //清空
vector::empty() //判断是否为空
vector::size() // 当前内容大小
vector::capacity() // 当前内存的容量
vector::resize() // 调整当前内容大小

dequeue

实现原理
dequeue在内存中是一段一段的连续空间。
如果需要新的空间,则在内存中开辟一段空间,通过指针连接到当前dequeue的头端或者尾端。
dequeue通过复杂的结构,维护了这些空间整体连续的假象,并且提供了随机存取的接口。

常用接口

deque::push_back()
deque::push_front()
deque::pop_back()
deque::pop_front()
vector::operator[]
queue::front() // 队列第一个元素
queue::back() // 队列最后一个元素

stack

实现原理
stack默认使用双向队列deque实现,加了适配器,修改接口

常用接口

stack::push()
stack::pop()
stack::top()
stack::empty()
stack::size()

queue

实现原理
queue使用双向队列deque实现,加了适配器,修改接口

常用接口

queue::front() // 队列头,先插进去的元素
queue::back() // 队列尾,后插进去的元素
queue::push()
queue::pop()
queue::empty()
queue::size()

list

实现原理
STL中的list就是一双向链表,可高效地进行插入删除元素。

常用接口

deque::push_back()
deque::push_front()
deque::pop_back()
deque::pop_front()
queue::front() 
queue::back() 

vector,list,dequeue

vector - 会自动增长的数组
deque - 拥有vectorlist两者优点的双端队列
list - 擅长插入删除的链表

priority_queue

实现原理
优先队列(堆),默认底层使用vector来实现。

说明
默认情况下是大顶堆,可以自定义为小顶堆

常用接口

priority_queue<int> Q;
int a[5]={3,4,5,2,1};
priority_queue<int> Q(a,a+5);
priority_queue<int, vector<int>, greater<int> > q;
小顶堆:greater<TYPE>
大顶堆:less<TYPE>

empty() 如果优先队列为空,则返回真 
pop() 删除第一个元素 
push() 加入一个元素 
size() 返回优先队列中拥有的元素的个数 
top() 返回优先队列中有最高优先级的元素

使用vector实现堆操作
algorithm中的make_heap,pop_heap,sort_heap来完成对vector的操作

#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main()
{
  int a[] = {15, 1, 12, 30, 20};
  vector<int> ivec(a, a+5);
  for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
    cout<<*iter<<" ";
  cout<<endl;

  make_heap(ivec.begin(), ivec.end());//建堆
  for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
    cout<<*iter<<" ";
  cout<<endl;

  pop_heap(ivec.begin(), ivec.end());//先pop,然后在容器中删除
  ivec.pop_back();
  for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
    cout<<*iter<<" ";
  cout<<endl;

  ivec.push_back(99);//先在容器中加入,再push
  push_heap(ivec.begin(), ivec.end());
  for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
    cout<<*iter<<" ";
  cout<<endl;

  sort_heap(ivec.begin(), ivec.end());
  for(vector<int>::iterator iter=ivec.begin();iter!=ivec.end();++iter)
    cout<<*iter<<" ";
  cout<<endl;

  return 0;
}

map

实现原理
使用红黑树实现,插入,查找,删除的效率都是log(n)

常用接口

std::map<char,int> first;
map::operator[] // 如果匹配则返回,不匹配就插入
map::insert() //也可以用来添加
查找
map<int, string> myMap;
if(myMap.find(key)==maplive.end()) // 没有
if(myMap.count(key)) // 找到有value的
删除
myMap.erase(key)

遍历一个map

map<string,int>::iterator it;
for(it=m.begin();it!=m.end();++it)
    cout<<"key: "<<it->first <<" value: "<<it->second<<endl;

set

实现原理
使用红黑树实现,类似于map只不过只有key,没有value

说明
元素唯一,插入后自动排序,插入,查找,删除效率都是logn

常用接口

std::set<int> s;
插入
insert()     
clear()          ,删除set容器中的所有的元素
empty()    ,判断set容器是否为空
size()      ,返回当前set容器中的元素个数
查找 
iter = s.find(2)) != s.end()
s.count(2) != 0
删除
s.erease(key)

multimap/multiset

实现原理
使用红黑树实现,插入,查找,删除的效率都是log(n)

说明
它允许重复键。multimap 中能存储重复键的能力大大地影响它的接口和使用。multimap不能使用下标操作符[],插入和读取。

常用接口
插入使用pair类型

multimap <string, string> DNS_daemon;
DNS_daemon.insert(make_pair("213.108.96.7","cppluspluszone.com"));

查找
count(k) 成员函数返回与给定键关联的值得数量。

遍历查找到的元素

multimap<string,int>::iterator it;
int num=m.count("Jack");
it = m.find("Jack");
cout<<"the search result is :"<<endl;
for(int i=1;i<=num;i++)
{
    cout<<(*it).first<<"  "<<(*it).second<<endl;
    it++;
}

multiset类似于multimap

unordered_map

实现原理
使用hash表实现,提供了和map一样的接口

说明
unordered_map来自于boost,后来收录到tr1中

与map的区别是,

  1. map使用红黑树实现,unordered_map使用hash表实现
  2. map中的key是有序的,unordered_map中的key是无序的。

string

实现原理
string也是stl中的一种容器,提供iterator和相关的接口

常用接口

s.empty()  判断是否为空,bool型
s.size() 或 s.length() 返回字符的个数
s[n]  返回位置为n的字符,从0开始计数
s1+s2 连接

int stoi (const string&  str, size_t* idx = 0, int base = 10);
//C++,把string转化为int,如果不是以数字开头的会出Exception
clear();//清除string中的内容
resize(0);
string substr(int pos = 0,int n = npos) const;//返回pos开始的n个字符组成的字符串
s.find ( " cat " ) ;  //超找第一个出现的字符串”cat“,返回其下标值,查不到返回 4294967295,也可查找字符;
s.append(args); //将args接到s的后面
s.compare ( " good " ) ;  //s与”good“比较相等返回0,比"good"大返回1,小则返回-1;
reverse ( s.begin(), s.end () );  //反向排序函数,即字符串反转函数

常用函数

find
InputIterator find (InputIterator first, InputIterator last, const T& val)
例如:

multimap<string,int>::iterator it;
int num=m.count("Jack");
it = m.find("Jack");

或者

it = find(m.begin(), m.end(), "Jack")

swap

template <class T> void swap ( T& a, T& b )
int x=10, y=20;                              // x:10 y:20
std::swap(x,y);                              // x:20 y:10
std::vector<int> foo (4,x), bar (6,y);       // foo:4x20 bar:6x10
std::swap(foo,bar);                          // foo:6x10 bar:4x20

reverse

std::reverse(myvector.begin(),myvector.end()); 

sort/stable_sort

std::sort (myvector.begin(), myvector.begin()+4);

bool myfunction (int i,int j) { return (i<j); }
int myints[] = {32,71,12,45,26,80,53,33};
std::vector<int> myvector (myints, myints+8);               // 32 71 12 45 26 80 53 33
// using function as comp
std::sort (myvector.begin()+4, myvector.end(), myfunction); // 12 32 45 71(26 33 53 80)

Effective C++ 读书笔记

条款1,视C++为一个语言联邦

可以把C++看成四个组成部分:

  • C语言的部分
  • Object Oriented C++ 继承封装多态
  • Template C++ 使用模板编程
  • STL

每一个部分都有各自的规约

条款2, 尽量以const,enum,inline替换#define

  • #define只有替换功能,在预处理阶段完成,没有类型检查,也没有封装性
  • 使用const替代变量定义,inline替代函数定义
  • 预处理器中,#include必不可少,#ifdef,#else可以用来进行控制编译

条款3,尽可能使用const

只要是事实,就把它说出来。只要是const就要声明为const类型。

  • const修饰变量
    const char p = greeting等价于char const p = greeting
    char * const p = greeting 指针不可更改指向对象
  • const修饰函数,是最有威力的应用

(1) const 返回值

(2) const 函数参数,使用最多

(3) const 成员函数,表明这个函数不能修改任何成员变量(static变量可以修改),也不能调用任何非const成员

补充,
volidate int a,告诉编译器这个值可能被未知因素修改,每次都要从内存中重新读取
mutable int a,可以突破const成员函数限制,在函数中被修改

条款4,确定对象被使用前已先被初始化

  • 成员初始化应该在构造函数之前,意味着要使用成员初始化列表进行成员变量的初始化
    说明:成员变量总是以声明的次序被初始化

  • 对于static变量,使用Singleton+inline,保证在对象使用前初始化

条款5,了解C++默默编写并调用了哪些函数

构造函数,拷贝构造函数,赋值函数,析构函数

条款6,若不想使用编译器自动生成的函数,就该明确拒绝

  • 如果某些对象不可复制(不能使用copy constructor)
    不是很安全的做法:把拷贝构造函数声明为private
    更好的做法:写一个UnCopyable基类,copy constructor声明为private

条款7,为多态基类声明virtual析构函数

class B:A{}
A *b=new B()
delete b

因为b是A类型的指针,所以会导致局部销毁(只有A的部分被销毁)

原则:

  • 企图作为(多态的)base class的类理论上都应该有virtual函数,否则不应该作为base class(虚指针会额外增加空间)
  • 任何带有virtual 函数的类都应该把析构函数声明为virtual
    不要试图继承任何STL容器,因为他们没有virtual的析构函数

条款8,别让异常逃离析构函数

  • 析构函数不能抛出异常,否则会导致不明行为。
  • 析构函数应该吞下这个异常,防止传播
  • 调用一个自己的函数,使得用户有机会来处理这个异常

条款9, 绝不要在构造或者析构过程中调用virtual函数

  • 构造过程

    class A{
    public:     
        A(){
             virtual fun()
        }
    }
    class B:A{}
    B b;
    

构造B->构造A->调用fun(),这时B还没构造完(被编译器看成A对象),导致virtual 函数不会下降到子类执行。

  • 析构过程
    析构B->析构A->调用fun(),这时B已经被析构掉了,同样virtual函数不会下降,得不到想要的结果。

条款10,令operator= 返回一个reference to *this

为了保证连续运算如:A=B=C 相当于A = (B = C)
返回一个引用,不会调用copy constructor
对于+=同样适用

条款11,在operator= 中处理自我赋值

判断一下,if (this == &rhs) return *this

条款12,复制对象时勿忘其每一个成分

可能出现的问题

(1)对象中的非内置类型不能得到赋值

(2)对象从父类继承而来的变量不能得到赋值

  • 赋值所有local成员(内置类型,对象)
  • 调用所有base class中的适当的copy constructor

条款13,以对象管理资源

C++申请释放的资源:堆内存(最常用),文件,互斥锁,数据库连接等。一旦申请资源,就必须释放,否则就会造成内存泄露。

以对象管理资源相当于,使用一个类(RAII类)封装这个资源,在构造时初始化,在析构时释放。声明这个对象时使用栈内存声明。

常用:

auto_ptr ,封装对象,重写了指针行为,看起来像一个指针。只能指向一个对象。复制或者赋值,会删除原来的指针。

shared_ptr,类似于auto_ptr,不过允许多个指针指向同一个对象,内部提供引用计数。
这两个是最常见的RAII类,在构造时初始化,析构时delete。(注意不能auto_ptr(new std::string[10])数组对象)

条款14,在资源管理类中小心copying行为

类似于auto_ptr或者shared_ptr的处理方式,对于复制。可以:

  • 禁止复制
  • 引用计数,类似于shared_ptr

条款15,在资源管理类中提供对原始资源的访问

隐式:如auto_ptr重写了指针行为,*ptr,ptr->使得这个变量看起来像一个指针。从而可以访问封装的资源

显示:提供get()函数返回资源

条款16,使用new和delete时要采用相同的形式

A *a=new A() ,释放时 使用delete a

int *a=new a[100],释放时使用delete []a

条款17,以独立语句将newed对象置入智能指针

std::tr1::shared_ptr<Widget> pw(new Widget)
processWidget(pwd, priority())

使用单独语句,不要放到一起可能会造成编译先后导致指针丢失。
其实不是很明白这点

条款18,让接口容易被使用,不易被误用

  • 导入新类型

    Date(int month, int day, int year)
    

多个参数,使用Month,Day,Year类型,可以预防接口被误用

  • 接口一致性

如:stl每个容器都有size()方法

条款19,设计class犹如设计type

设计一个类时需要考虑很多问题:

  1. 创建和销毁
  2. 初始化(初始化列表),拷贝构造函数
  3. pass by value && pass by reference
  4. 继承关系
  5. 类型转换
  6. 操作符重载
  7. 标准函数驳回(private copy constructor)
  8. public private
  9. 效率,异常
  10. 不够一般化,太过一般化
  11. 是否真的需要这个类型

条款20, 宁以pass by reference to const 替换 pass by value

  • 区别

pass by value:

要调用copy constructor,可能是费时的操作

pass by reference to const:

const Student &s,const保证变量在函数内不会被修改

  • pass by value可能导致多态失效

    void printNameAndDisplay(Window w)
    

传入子类对象,不能实现多态

  • 在编译器底层,reference是通过指针来实现的

条款21,必须返回对象时,别妄想返回其reference

const Rational operator* (const Rational &lhs, const Rational &rhs)

如果返回reference

  • 返回local stack的对象(Rational r),则函数退出时,这个对象已经被销毁了
  • 返回heap-allocate对象,会造成何时delete的问题。
  • 返回static对象,if(ab == cd),导致一个static对象不够用的问题

原则,必须在返回reference和object作出一个选择,程序员的工作就是选出正确的那个

条款22,将变量声明为private

  • public接口内全部都是函数,可以产生用户使用这个类时,良好的一致性
  • private parameter可以产生封装的效果,封装使得变更更加容易
  • 假如有一个public变量,如果取消它,所有使用它的客户代码都会被破坏
    假如有一个protect变量,如果取笑它,所有使用它的derived class都会被破坏
    所以protect并不比public更具有封装性

条款23,宁以non-member、non-friend替换member函数

  • 多个操作具有先后顺序,应该把他们绑定到一起
  • 封装->客户端难修改->更多弹性去改变
  • non-member(non-friend)函数VSmember函数

non-member函数不能访问private成分,提供更大的封装性

条款24,若所有参数皆需类型转换,请为此采用non-member函数

实现有理数类Rational,乘法的操作符重载
开始可能会向使用成员函数的写法

const Rational operator*(const Rational & rhs) const

但是希望完成乘法交换律

Rational r
Rational result = 2 * r

需要对2进行隐式类型转换,方法

const Rational operator*(const Rational &lhs, const Rational &rhs) 

使用non-member函数。

不是很明白

条款25,考虑写出一个不抛出异常的swap函数

std::swap(T& a, T& b)可以对两个对象进行交换

如果这样做的效率不高,可以考虑自己写一个不会抛出异常的swap成员函数

例如:stl 容器中就有很多swap函数,只交换指针,而不会复制对象。

  1. 自行实现这样一个swap成员函数(可以使用std::swap调换指针)
  2. 在命名空间内提供一个swap(Widget &a,Widget &b)去实现一个非成员函数来调用前者。

条款26,尽可能延后变量定义式的出现时间

对变量进行定义,意味着承受构造的成本。

原则:应该延后变量定义到使用前的一刻为止。

条款27,尽量少做转型动作

C风格的转型

(int)2.1
int(2.1)

C++的新式转型:

  • const_cast<T>(expression) 将对象的常量性移除
  • dynamic_cast<T>(expression) 主要用来进行安全向下转型
    例如:只有基类可以使用,但是想调用子类的函数。尝试使用多态来代替。
  • static_cast<T>(expression) 主要用来强制类型转换
    例如:static_cast<int>(2.1)
    尽量使用C++风格的转型

条款28,避免返回handles指向对象内部成分

class A{
publicvoid func();
}
class B{
private:
     A *a
}

如果在B类中提供A&的返回(假设为rt),那么用户可以调用rt.func()修改B中的private成员了。
这是一种放松封装的行为。

条款29,为“异常安全”而努力是值得的

异常安全的函数提供以下三个保证之一(从弱到强):

  • 基本承诺:如果抛出异常,程序内的任何事物仍然保持在有效状态下
  • 强烈保证:函数调用成功,则完全成功。函数调用失败,则程序回复到调用之前的状态
  • nothrow:保证绝对不抛出异常。(通常完全使用内置类型的操作,提供不抛出异常的保证)
    一个软件系统,要么具备异常安全性,要么不具备。只提供部分异常安全性函数,不能叫做具备异常安全性的系统。
    以对象管理资源,是一种很好的防止内存泄露,保证异常安全性的方法。

条款30,透彻了解inlining的里里外外

  • inline函数意味着对这个函数的每一次调用,使用函数本体替换

好处:减少调用成本

坏处:增加代码体积

  • inline函数适合小型被频繁调用的函数

函数内部有for循环不适合inline,因为本身的开销已经够大,减少调用的开销意义不大。

  • inline只是一个向编译器发出的申请,编译器可以忽略它。

如编译器拒绝复杂函数inline(带有递归,循环),virtual函数也会使inline落空。

条款31,将文件间的编译依存关系降到最低

方法1,使用Handle class

增加一个实现类去真正实现类的功能,原来的类只维护一个指向实现类的指针

方法2,使用Interface class

基类是虚基类,不包括任何成员变量。

条款32,确定你的public继承是is-a的关系

如题

条款33,避免遮掩继承而来的名称

假如:Derived:Base

当编译器通过函数名称去找相应函数,会先从Derived类作用域找,然后再从Base类的作用域找
当使用函数重载的时候就可能出现问题。

使用using Base::func可以避免这种情况。

条款34,区分接口继承和实现继承

  • 对于non-virtual函数的继承

意味着,子类必须有和父类一样的实现

  • 对于virtual

(1)pure-virtual, 只继承接口,意味着每个子类的行为都很有可能不一样

(2)imprure-virtual, 提供缺省的实现,意味着有一些子类的行为可能一样

可以使用pure-virtual+缺省行为分离(另外写一个函数)的方法,解决有可能子类在不知情的情况下继承了并不需要的缺省的实现。

条款35,考虑virtual函数以外的其他选择

  • NVI Non-virtual Interface
    使用public non-virtual 函数调用private virtual函数(做一下修饰而已)

  • 使用函数指针

  • 使用tr1::function封装函数指针,代替函数指针的行为

  • 使用strategy设计模式

将想要virtual的行为封装成一个类(Calculator),在类内部进行多态计算,通过传入的对象指针来判断。

条款36,绝不重新定义继承而来的non-virtual函数

class A{
public:
     void fun()
}
class B:A{
public:
     void fun()
}
A *ptA=new B()
B *ptB=new B()

ptA->fun()调用A中的fun
ptB->fun()调用B中的fun
因为non-virtual函数不能进行动态绑定,调用函数只跟指针类型有关,所以

  1. 不要重写父类的non-virtual函数
  2. 父类的non-virtual函数意味着,所有子类的实现都是这样

条款37,绝不重新定义进程而来的缺省参数值

缺省参数都是静态绑定的,即使是在virtual的函数中

条款38,复合(组合)是has-a的关系

条款39,明智而审慎地使用private继承

private继承意味着所有父类的成员在子类中都变为private,

好处:可以让基类部分最优化,减少尺寸。

条款40,明智而审慎地使用多重继承

  • 一个class继承自多个base class,那么父类成分有相同函数,就需要显示指定。
  • 对于钻石型继承,B:A,C:A,D:B,D:C,需要指定虚继承,来避免重复继承A中的成分
  • 虚继承需要编译器做很多工作,要付出一定成本,一般不用。
  • 如果有单一继承可以满足需求,一般这个方案一定比多重继承要好。

条款41,了解隐式接口和编译器多态

  • 运行时多态,通过虚指针和虚函数实现

  • 编译时多态

(1) 函数重载,相同函数名不同参数列表

(2) 在模板特化的时候,根据类型生成具体的函数

条款42,了解typename的双重意义

template< class T> class Widget;
template<typename T>class Widget;

并没有什么不同

当使用嵌套从属名称,如:

template<typename C>
typename C::const_iterator iter(container.begin())

const_iterator是依赖于C的名称,这时候必须用typename

条款43,学习处理模板化基类内的名称

对于模板C++的继承,由于基类模板可能被特化,特化使得基类内的成员不确定,C++会拒绝从模板化基类中寻找继承而来的名称

解决办法:

  1. 在使用base class之前使用this->
  2. 使用using

条款44,将与参数无关的代码抽离templates

使用带参template可能会引起代码膨胀,如:

template<typename T,std:size_t n>

解决办法:
使用模板父类去处理由于size_t而造成的代码膨胀的问题

条款45,运用成员函数模板接受所有兼容类型的参数

  • 智能指针是使用模板实现的,那如果我们要智能指针之间(具有继承关系的)能够相互转化,赋值,解决办法:
  • 使用成员函数模板,对兼容的类型进行构造和赋值

条款46,需要类型转换时请为模版定义非成员函数

Rational<int> a(1,2);
Rational<int> result = a*2; // Error

模板化实例,不进行隐式类型转换,使用friend方法。

条款47,请使用traits classes表现类型信息

引用:

traits class是个类模板,在不修改一个实体(通常是数据类型或常量)的前提下,把属性和方法关联到一个编译时的实体。在c++中的具体实现方式是:首先定义一个类模板,然后进行显式特化或进行相关类型的部分特化。
我的理解是:traits是服务于泛型编程的,其目的是让模板更加通用,同时把一些细节向普通的模板用户隐藏起来。当用不同的类型去实例化一个模板时,不可避免有些类型会存在一些与众不同的属性,若考虑这些特性的话,可能会导致形成的模板不够“泛型”或是过于繁琐,而traits的作用是把这些特殊属性隐藏起来,从而实现让模板更加通用。

条款48,认识template元编程

  • 模版元编程有两个效力:第一,它让某些事情更容易;第二,可将工作从运行期转移到编译期。
  • 引用:

    所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。普通用户对 C++ 模板的使用可能不是很频繁,大致限于泛型编程,但一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免的都大量地使用 C++ 模板,一个稍有规模的大量使用模板的程序,不可避免的要涉及元编程(如类型计算)。

条款49,了解new_handler的行为

new_handler 的意思就是说,当使用operator new 无法分配内存时,转交给用户,用户来做一些事情。

条款50,了解new和delete的合理替换时机

有时候,我们替换掉编译器提供的new或者delete。重写operator new。三个常见理由:

  1. 用来检测运用上的错误,超额分配一些内存,再额外的空间放置一些内存;
  2. 为了强化效能,编译器提供的new/delete是通用的,通用就意味着冗余和效率低下,为什么?这个很好理解,因为他要支持很多情况下,也必须考虑很多情况。我们重写new/delete,也就是说,对于特定情况,给出特定的实现。
  3. 为了收集使用上的统计数据。

条款51,编写new和delete时需固守常规

自定义new/delete的时候,需要遵守一些规则。

  1. 循环申请,直到成功或者抛出异常
  2. class专属版本处理,分配大小与class大小不一致的错误。
  3. delete的时候,判断是否为null。

条款52,写了placement new也要写placement delete

条款53,不要轻忽编译器的警告

条款54,让自己熟悉包括TR1在内的标准程序库

C++11(原名C++0x)于2011年8月12日公布。
TR1是一份文档,由编译器实现,在std::tr1命名空间下
C++11纳入了大部分TR1的内容

条款55,让自己熟悉Boost

Boost是一个社区,提供很多程序库,作为新的C++标准的试验场。


2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:205394次
    • 积分:3014
    • 等级:
    • 排名:第12884名
    • 原创:106篇
    • 转载:18篇
    • 译文:1篇
    • 评论:30条
    最新评论