C++ Primer阅读笔记

2018/06/28 C 共 9858 字,约 29 分钟

第二章 变量和基本类型

特殊类型

size_t是unsigned类型,用于指明数组长度或下标,它必须是一个正数,std::size_t.设计size_t就是为了适应多个平台,其引入增强了程序在不同平台上的可移植性。

ptrdiff_t是signed类型,用于存放同一数组中两个指针之间的差距,它可以使负数,std::ptrdiff_t.同上,使用ptrdiff_t来得到独立于平台的地址差值,是单元个数,不是字节长度

size_type是unsigned类型,表示容器中元素长度或者下标,vector::size_type i = 0;

difference_type是signed类型,表示迭代器差距,vector:: difference_type = iter1-iter2.

前二者位于标准类库std内,后二者专为STL对象所拥有。

初始化

int a ; 没有初始化内容,它的值由系统分配规则是在栈区和堆区(函数内定义或者类里面定义)都取随机值,在全局区(全局常量,静态变量)都是全零值。

类类型的初始化其实就是构造函数, 隐式初始化实际上就调用类型的默认构造函数来初始化类型对象;直接初始化是调用类的拷贝构造函数;而直赋值初始化是调用赋值操作符重载和拷贝构造函数。相关内容再后续章节会有详细说明。

extern

关于extern,对于变量可以其他地方extern声明访问,变量需来源也加extern

extern const int b(2); 这样定义的常量才可以被其他相关文件(head.h)声明并使用

int a(1);、 这样定义的变量可以被其他相关文件(head.h)声明并使用

常量如果在其他地方被申明和使用一定要在定义的时候加上关键字extern因为如果不加的话常量默认作用范围是定义它的那个文件,变量默认是所有相关文件。

const限定符

常量分为两类 编译时常量 和 运行时常量(在.NET中标示为readonly) 编译时常量是定义后直接初始化的常量,运行时常量值要初始化的值必须要通过代码运行才可以确定的

const int a(1); // 编译时常量

const int b = getval(); // 运行时常量值来自一个函数的运行结果

const myclass my(1,"tom"); // 自定义类的常量定义都是运行时常量,因为需要运行类的构造函数

非常量引用类型必须严格匹配。 如果对非const引用b不做类型匹配限制,b实际就会引用临时变量temp,对b的修改无法反应到变量a,引用失去了其意义。

typedef

这个关键字非常有用,用来给某个类型指定一个别名,比如

typedef int zx ;

int a(1) ; // 等同于 zx a(1)
double* (*a)[3][6];
typedef (double *) ArrType [3][6];
ArrType * a;

函数传数组会变成指针int Sum(int i[])

Sum的本意是用sizeof得到数组的大小,然后求和。但是实际上,传入自函数Sum的,只是一个int 类型的指针,所以sizeof(i)=4,而不是24,所以会产生错误的结果。解决这个问题的方法使是用指针或者引用。

使用指针的情况: 1 int Sum(int (i)[6]) sizeof(i)/sizeof(int) //sizeof(i) = 24 在这个Sum里,i是一个指向i[6]类型的指针,注意,这里不能用int Sum(int (i)[])声明函数,而是必须指明要传入的数组的大小,不然sizeof(*i)无法计算。但是在这种情况下,再通过sizeof来计算数组大小已经没有意义了,因为此时大小是指定为6的。

使用引用的情况和指针相似: int Sum(int (&i)[6]) sizeof(i)/sizeof(int)

这种情况下sizeof的计算同样无意义,所以用数组做参数,而且需要遍历的时候,函数应该有一个参数来说明数组的大小,而数组的大小在数组定义的作用域内通过sizeof求值。

枚举

枚举是一组可选常量值,既然是一组可选值说明包含多个常量。枚举定义语法如下

enum val{val1 = 2, val2 = 4, val3}  

最后一个内容没有显示给值等价于 val3 = 5 。如果不指定值默认第一个值从0开始下一个依次+1递增。枚举的每一项都是一个唯一的const类型值

枚举项和int类型值有对应关系,但是二者只能单向转换,枚举可以自动转成int,而int却不能转成枚举

val a = val2 ; // 枚举之间赋值初始化
int b = val2 ; // 枚举转成int并初始化 
val a = 2 ;     // int 不能转成枚举,无法初始化

头文件

类定义要放在头文件里。这样如果某个文件需要这个类只需要把头文件include进来即可。

运行时常量也可以在头文件定义,表达式可以在包含文件中定义。这样就能实现在在不同的包含文件得到不同的常量值。

不管你的头文件会不会被多个文件引用,你都要加上这个,在头文件里写。一般格式是这样的:

    #ifndef <标识>
    #define <标识>
    ...... 头文件代码
    #endif

第三章 标准库类型

标准库类型

using声明是对某个命名空间做引入。主要作用是简化代码编写。

Vector<T> v3(n, i) ; 

v3包含n个值为i的元素 参数 T 如果是类类型则一定要有拷贝构造函数(未定义的情况下系统会自动分配一个)

Vector<T> v4(n) ;    

v4含值初始化的元素个副本 参数 T 如果是类类型则一定要有默认构造函数(未定义的情况下系统会自动分配一个)

如果是内置类型则分配n个0

第四章 数组与指针

对于类类型来说:myclass ls[2] ; 无论在是全局定义还非全部定义都会调用类型的默认构造函数,如果类没有默认构造函数则编译出错

char a[]{'a', 'b', 'c'} ; 
// 标准定义 

char b[]{'a', 'b', 'c', '\0'} ;  
// 一个C风格的字符串,等价于 char b[] = "abc" 二者的字符数组大小都是4

上面两种定义方式唯一不同的就是最后一个字符是否是 ‘\0’ (结束符)如果是则表明定义了一个C风格的字符串。

要注意:虽然char b[] = “abc” 内容只有三个字符,由于这种定义是C风格字符串的定义法所以系统会自动在对应的字符数组后面加上 ‘\0’

在指定大小定义C风格字符串时要注意前后长度匹配 char b[3] = “abc” 会产生错误,需要存放四个字符大小。

表示数组下标和数组大小的数据类型和bitset一样是size_t

int *cur = 0 ;  

可以给指针赋予初始0值表示该指针没有指向任何对象,不应对其有任何操作 。这个赋值表达式中0值是有约束的,必须是字面常量或者编译时常量值0

int *cur = 121 ; 

也可以给指针赋予一个整数常量,但最好不要这么做。因为该常量表示内存地址而我们并不十分清楚这个地址中存放了什么数据。一单指针做了某些操作就可能破坏这些数据,对一个未知的内存做指针指向是危险的(未初始化指针情况也类似)

有一类比较灵活的指针 *void 可以指向任何类型的变量. 这类指针表示指向某个内存地址的数据,但不清楚改数据类型。void指针操作有限。由于不知道指向的数据类型所以不能对指向数据做任何操作。

两个数组指针可以相减,值是类型为 ptrdiff_t 的项间距(两个操作数中不能有哨兵位)

数组最重要的是下标操作。数组指针也有下标操作,但是意义和数组下标不一样

    int a[] = {1,2,3,...} ;  
    int *cur = &a[1] ;
    cur[1] = 2 ;  // 等价于 *(cur + 2) = 2 也等价于 a[3] = 2
    int i = cur[-2] ;  // 等价于 int i = *(cur - 2) 也等价于 int i = a[1]
    char a[]{'a','b','c','\0'} ;  // 第一种数组定义语法
    char a[] = “abc” ;  // 第2种数组定义语法
    char *a = “abc” ;  // 指针表示法

strlen

char s1[10] = {'m','o','b','i','l'};
char s2[20] = {'A','N','S','I','\0','C','+','+'};   
char s3[6]  = {'I','S','O','C','+','+'};
    
cout<<strlen(s1)<<endl; // 5
cout<<strlen(s2)<<endl; // 4
cout<<strlen(s3)<<endl; // 不确定

可以创建动态数组,动态数组创建时更具类型不同其初始化值也不同。

// 内置类型
int *a = new int[10] ; // 包含10个未初始化元素的数组

int *b = new int[10]() ; // 包含10个初始化为0的元素数组

// 类类型
string *c = new string[10] ; // 包含10个调用了类默认构造函数完成初始化的元素数组

第五章 表达式

c++ 中箭头操作是个复合操作,将解引和调用组合调用了

new delete

开辟单变量地址空间

int *a = new int ;  
// 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址. (和对象初始化一样:内置类型是否全局决定是否初始化,类类型无论如何都会调用默认构造函数来初始化) 

int *a = new int(5) ; // 作用同上,但是同时将整数赋值为5  

const int *a = new const int(5) ; // 作用同上,但是定义的是一个常量整数  

开辟数组空间

一维: int *a = new int[100] ; // 开辟一个大小为100的整型数组空间

二维: int **a = new int[5][6] ;

三维及其以上:依此类推.

一般用法: new 类型 [初值]

虽然回收了动态内存,但是这时候指针依然指向原来的地址叫做悬垂指针。如果在对该指针做操作可能会破坏内存数据。因此释放了数据空间后应该让指针指向 0 (a = 0)

类型转换

显示转换需要借助系统提供的转换函数来实现

static_cast、dynamic_cast、reinterpret_cast和const_cast

  • static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
  • static_cast 用于良性转换,一般不会导致意外发生,风险很低。
  • const_cast 用于 const 与非 const、volatile 与非 volatile 之间的转换。
  • reinterpret_cast 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。
  • dynamic_cast 借助 RTTI,用于类型安全的向下转型(Downcasting)。

第六章 语句

过

第七章 函数

指针和引用

需要注意的是指针。在函数内对指针参数本身(指针的指向地址)的更改同样不会影响调用实参。不过对指针指向的内容更改却是有影响的。

不能给引用传字面量常量,但可以给const 引用传

如果想严格匹配实参和形参数组大小可使用 引用数组,定义方式如下:

void funct(int (&arr)[10]) ; 

表示是一个数组引用,且数组大小是10 。 圆括号必须因为下标操作具有更高优先级

返回值遵循一个安全约束,即 不允许返回局部对象的引用或指针

// 返回引用
int &funct(int a, int &b) 
{
    int c = 1; 
    return c; // 标准局部对象 不允许返回

    return a;  // 传参时拷贝的实参,实际值只有在函数内有效,不允许返回
    return b;  // 通过引用形参 传递的是外部实参的引用,可以传递
}


// 返回指针
int *funct(int a, int *b) 
{
    int c = 1; 
    return *c; // 标准局部对象 不允许返回

    return *a;  // 传参时拷贝的实参,实际值只有在函数内有效, 不允许返回
    return b;   // 传参时拷贝的实参,虽然指针是局部对象但他指向的值是外部数据,可以传递
}

总之判断返回是否是局部变量可以看返回值的实际值是作用范围否只在函数体内。

局部对象

局部对象的作用范围限制在函数内,函数执行完毕局部变量会被系统自动销毁。

不过静态局部对象不会销毁,所有静态对象一旦创建就会一直存在直到应用程序关闭。

静态对象初始化只会执行一次:

void funct() 
{
    static int a(0); // 只会执行一次,第二次执行到代码处会检查静态变量 a 是否存在若存在则跳过
    a++;
    cout << a << endl;    

} // 函数到此执行完毕并清理局部对象,但不会清理静态对象,所以a依然存在,值为最后操作结果。 a虽然不会被清理但只限在定义的函数体内访问

funct() ; // 输出 1
funct() ; // 输出 2
funct() ; // 输出 2

内联函数

内联函数是指编译时将调用函数的地方用实际函数体语句替代的一类函数,函数定义时返回值前加上 inline 就表示函数内联

bool c = funct(a, b) ; 

编译时实际会被替换为 bool c = (a > b);

内联函数有较好的性能,因为函数在调用时系统刚要分配栈空间,内联函数会直接展开代码,所以不会有栈空间分配步骤 (内联函数体内数据需要的空间会在调用它的函数执行时一并分配)。

内联函数的特点决定了如果修改函数则所有用到内联函数的地方都要重新编译。

类的成员函数

类成员函数包含一个隐藏参数 this 指针。它指向成员函数所属的当前类对象。

类成员函数名后面可以用const修饰,其作用是表示 this 指针指向常量,也就是说不允许修改当前类的任何成员

函数重载

不但参数类型和个数可以作为重载依据,当形参数是引用或指针时 const 可用作重载依据。

指向函数的指针

指针不但可以指向内置类型,类类型,数组,还可以指向函数。函数指针的最大作用是将函数作为一种类型传递。

定义语法是:

void (*pr)(int i, string s) ; 
// 定义了一个函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型

pr = funct ; 
// 为定义的指针赋值(funct是函数名称)

pr(1,"str") ;  
//  通过指针调用函数(不用解引)


可以用 typedef 可简化定义,将定义一个变为定义一类


typedef void (*pr)(int i, string s) ; 
// 定义了一类函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型

pr f1 = funct ; // 定义类型变量

对于有重载的函数,定义的指针一定要有对应的重载版本。 初始化指针变量时系统也会查找对应的重载函数

第八章 标准IO库

缓冲区

缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

1、全缓冲

在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。

2、行缓冲

在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。

3、不带缓冲

也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

缓冲区的刷新

下列情况会引发缓冲区的刷新:

  1、缓冲区满时;

  2、执行flush语句;

  3、执行endl语句;

  4、关闭文件。

可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:

cout«flush ; // 将显存的内容立即输出到显示器上进行显示

键盘操作演示行缓冲

当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。

IO对象无法复制或者赋值,所以io对象作为函数形参或返回值时只能使用指针或引用。

第九章 顺序容器

vector : vector和built-in数组类似,是一个在堆上建立的一维数组,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符。vector因为存储在堆上,所以支持erase(), resieze()(重新划分容器容量)等操作; vector不用担心越界当空间不够用的时候,系统会自动按照一定的比例(对capacity( )大小)进行扩充。在vector序列末尾添加(push_back( ))或者删除(pop_back())对象效率高,在中间进行插入或删除效率很低,主要是要进行元素的移动和内存的拷贝,原因就在于当内存不够用的时候要执行重新分配内存,拷贝对象到新存储区,销毁old对象,释放内存等操作,如果对象很多的话,这种操作代价是相当高的。为了减少这种代价,使用vector最理想的情况就是事先知道所要装入的对象数目,用成员函式 reserve( ) 预定下来;vector最大的优点莫过于是检索(用operator[ ])速度在这三个容器中是最快的。

list : list的本质是一个双向链表(根据sgistl源代码),内存空间不连续,通过指针进行操作。说道链表,它的高效率首先表现是插入,删除元素,进行排序等等需要移动大量元素的操作。显然链表没有检索操作operator[ ], 也就是说不能对链表进行随机访问,而只能从头至尾地遍历,这是它的一个缺陷。list有不同于前两者的某些成员方法,如合并list的方法splice( ), 排序sort( ),交换list 的方法swap( )等等。

deque : deque是一个double-ended。queue是由多个连续内存块构成,deque是list和vector的兼容,分为多个块,每一个块大小是512字节,块通过map块管理,map块里保存每个块得首地址。因此该容器也有索引操作operator[ ],效率没vector高。另外,deque比vector多了push_front( ) & pop_front( )操作。在两端进行此操作时与list的效率 差不多。

顺序容器还有三种适配器,主要作用是包装其它容器使之具有某种操作特征。

1 stack 堆栈适配器 ( 可用的容器类型 vector deque list)

2 queue 队列适配器 ( 可用的容器类型 deque list)

3 priority_queue 优先级队列 (可用的容器类型 deque vector)

std::stack< int,std::list<int> > charStack; 

第十章 关联容器

关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。

Map和set

map

对于键类型,唯一的约束就是必须支持 < 操作符,至于是否支持其他的关系或相等运算,则不作要求。

map 对象的元素是键-值对,其 value_type 是存储元素的键以及值的 pair 类型,而且键为 const。

在学习 map 的接口时,需谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改。

关联容器的元素按键排序和访问。关联容器支持通过键高效地查找和读取元素。键的使用,使关联容器区别于顺序容器,顺序容器的元素是根据位置访问的。

map 和 multimap 类型存储的元素是键-值对。它们使用在 utility 头文件中定义的标准库 pair 类,来表示这些键-值对元素。对 map 或 multimap 迭代器进行解引用将获得 pair类型的值。pair 对象的first 成员是一个 const 键,而 second 成员则是该键所关联的值。set 和 multiset 类型则专门用于存储键。在 map 和 set 类型中,一个键只能关联一个元素。而multimap 和 multiset 类型则允许多个元素拥有相同的键。

关联容器共享了顺序容器的许多操作。除此之外,关联容器还定义一些新操作,并对某些顺序容器同样提供的操作重新定义了其含义或返回类型,这些操作的差别体现了关联容器中键的使用。

第十一章 范型算法

就是sort那些,过

第十二章 类

还有一点非常重要,想要定义类的const对象必须显示定义对应构造函数,无法依赖系统自动分配的构造函数。

class mycls
{
    public:
        mycls(){}; // 想要定义 const mycls a; 必须要显示定义默认构造函数
        mycls &Get(){ return *this; };
        const mycls &Get() const { return *this; }; // 想如果const函数返回this引用或指针; 必须要返回const指针或引用,因为无法用const对象(this)初始化非const对象。

};

如果数据成员是自定义类类型,如果不显示初始化则类一定要有默认构造函数否则编译错误,成员被初始化的次序就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。

可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:mycls obj(“tom”), 无法用 mycls obj = “tom” 因为转换被禁止,通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。

explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。

// 类成员友元
class me; // 先要前向声明类

class he  // 友元类需要目标类做参数由于目标类已声明所以可以使用类引用或者指针--show(me &it)方法中的参数
{
    public:
        void show(me &it);
};

class me // 目标类需要声明类的的成员show作为自己的友元函数,he在上面做了成员声明所以成员show(me &it)可用
{
    friend void he::show(me &it);
    
    private:
        int i;
        string s;
};

void he::show(me &it) // 友元方法中使用目标类私有成员,目标类上一步定义了私有成员因此这里成员可用
{
    cout << it.i << it.s << endl;
};

类的静态成员不属于任何一个类对象所以静态成员中(主要是静态方法)不包含this指针因此也无法声明成const函数,它是所有类对象共享数据。

不同于其他语言的访问方式,静态成员既可以通过类型访问:myclass::staticname() 也可以通过类对象(对象,指针或者引用)访问

一般来说类数据成员在类定义体内不能初始化化,但有个特例 const static 数据成员就可以在类的定义体中进行初始化 。

类非 static 数据成员在类体内声明,必须要在类体外定义。

第十三章 复制控制

复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。

复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。

class myclass
{
   public:
      myclass(const myclass &obj){}; // 复制构造函数

      ~myclass(){}; // 析构函数
 
      myclass& operator=(const myclass &obj){}; // 赋值操作符

   private:
     int age;
     string name;
}

复制构造函数

有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。

如果想禁止复制可以显示声明私有的复制构造函数(最好不要这么做否则类只能作为指针或引用传递)

赋值操作符

过

第十四章 重载操作符与转换

操作数至少要包含一个类类型,防止用户修改内置类型的操作符,如果用户定义 int operator+(int a,int b) 意味着用户要修改int类型的加法操作符。

第十五章 面向对象编程

动态绑定需要符合两个条件:调用函数必须是virtual ;必须要通过指针或引用调用虚函数。 动态绑定时执行函数取决于实际执行的类型,而不取决于指针或引用变量类型。

virtual函数版本是在运行时确定,非virtual函数是在编译时确定。

第十六章 模板与范型编程

过

Search

    Table of Contents