对 C 语言指针最详尽的讲解

指针对于C来说太重要。然而,想要全面理解指针,除了要对C语言有熟练的掌握外,还要有计算机硬件以及操作系统等方方面面的基本知识。所以本文尽可能的通过一篇文章完全讲解指针。

 

为什么需要指针?

 

指针解决了一些编程中基本的问题。

 

第一,指针的使用使得不同区域的代码可以轻易的共享内存数据。当然小伙伴们也可以通过数据的复制达到相同的效果,但是这样往往效率不太好。

因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。

但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)。

 

第二,指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树等等。

 

第三,有些操作必须使用指针。如操作申请的堆内存。

还有:C语言中的一切函数调用中,值传递都是“按值传递”的。

如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成。

 

 

指针是什么?

 

 

我们知道:C语言中的数组是指一类类型,数组具体区分为  int 类型数组,double类型数组,char数组 等等。

同样指针这个概念也泛指一类数据类型,int指针类型,double指针类型,char指针类型等等。

 

通常,我们用int类型保存一些整型的数据,如 int num = 97 , 我们也会用char来存储字符:char ch = ‘a’。

 

我们也必须知道:任何程序数据载入内存后,在内存都有他们的地址,这就是指针。

而为了保存一个数据在内存中的地址,我们就需要指针变量。

 

因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

 

 对 C 语言指针最详尽的讲解

 

 

为什么程序中的数据会有自己的地址?

 

弄清这个问题我们需要从操作系统的角度去认知内存。

 

电脑维修师傅眼中的内存是这样的:内存在物理上是由一组DRAM芯片组成的。

 

对 C 语言指针最详尽的讲解

 

而作为一个程序员,我们不需要了解内存的物理结构,操作系统将RAM等硬件和软件结合起来,给程序员提供的一种对内存使用的抽象。

这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。

所有的虚拟地址形成的集合就是虚拟地址空间。

 

对 C 语言指针最详尽的讲解

 

在程序员眼中的内存应该是下面这样的。

 

对 C 语言指针最详尽的讲解

 

也就是说,内存是一个很大的,线性的字节数组(平坦寻址)。每一个字节都是固定的大小,由8个二进制位组成。

最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。

如上图中,这是一个256M的内存,他一共有256x1024x1024  = 268435456个字节,那么它的地址范围就是 0 ~268435455  。

 

由于内存中的每一个字节都有一个唯一的编号。

因此,在程序中使用的变量,常量,甚至数函数等数据,当他们被载入到内存中后,都有自己唯一的一个编号,这个编号就是这个数据的地址。

指针就是这样形成的。

 

下面用代码说明

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#include <stdio.h>
int main(void){ char ch = 'a'; int num = 97;    printf("ch 的地址:%p",&ch);   //ch 的地址:0028FF47    printf("num的地址:%p",&num);  //num的地址:0028FF40 return 0;}

 

对 C 语言指针最详尽的讲解

 

指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。

指针的值(虚拟地址值)使用一个机器字的大小来存储。

也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~2w - 1 ,程序最多能访问2w个字节。

这就是为什么xp这种32位系统最大支持4GB内存的原因了。

 

我们可以大致画出变量ch和num在内存模型中的存储。(假设 char占1个字节,int占4字节)

 对 C 语言指针最详尽的讲解

 

 

变量和内存

 

为了简单起见,这里就用上面例子中的  int num = 97 这个局部变量来分析变量在内存中的存储模型。

 对 C 语言指针最详尽的讲解

 

已知:num的类型是int,占用了4个字节的内存空间,其值是97,地址是0028FF40。我们从以下几个方面去分析。

 

1、内存的数据

 

内存的数据就是变量的值对应的二进制,一切都是二进制。

97的二进制是 : 00000000 00000000 00000000 0110000 , 但使用的小端模式存储时,低位数据存放在低地址,所以图中画的时候是倒过来的。

 

2、内存数据的类型

 

内存的数据类型决定了这个数据占用的字节数,以及计算机将如何解释这些字节。

num的类型是int,因此将被解释为 一个整数。

 

3、内存数据的名称

 

内存的名称就是变量名。实质上,内存数据都是以地址来标识的,根本没有内存的名称这个说法,这只是高级语言提供的抽象机制 ,方便我们操作内存数据。

而且在C语言中,并不是所有的内存数据都有名称,例如使用malloc申请的堆内存就没有。

 

4、内存数据的地址

 

如果一个类型占用的字节数大于1,则其变量的地址就是地址值最小的那个字节的地址。

因此num的地址是 0028FF40。内存的地址用于标识这个内存块。

 

5、内存数据的生命周期

 

num是main函数中的局部变量,因此当main函数被启动时,它被分配于栈内存上,当main执行结束时,消亡。

    

如果一个数据一直占用着他的内存,那么我们就说他是“活着的”,如果他占用的内存被回收了,则这个数据就“消亡了”。

C语言中的程序数据会按照他们定义的位置,数据的种类,修饰的关键字等因素,决定他们的生命周期特性。

实质上我们程序使用的内存会被逻辑上划分为:栈区,堆区,静态数据区,方法区。

不同的区域的数据有不同的生命周期。

 

无论以后计算机硬件如何发展,内存容量都是有限的,因此清楚理解程序中每一个程序数据的生命周期是非常重要的。

 

指针变量和指向关系

 

用来保存指针的变量,就是指针变量。

如果指针变量p1保存了变量 num的地址,则就说:p1指向了变量num,也可以说p1指向了num所在的内存块 ,这种指向关系,在图中一般用 箭头表示。

 对 C 语言指针最详尽的讲解

 

上图中,指针变量p1指向了num所在的内存块 ,即从地址0028FF40开始的4个byte 的内存块。

 

定义指针变量

 

C语言中,定义变量时,在变量名前写一个 * 星号,这个变量就变成了对应变量类型的指针变量。必要时要加 ( ) 来避免优先级的问题。

 

引申:C语言中,定义变量时,在定义的最前面写上 typedef ,那么这个变量名就成了一种类型,即这个类型的同义词。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int a ; //int类型变量 aint *a ; //int* 变量aint arr[3]; //arr是包含3个int元素的数组int (* arr )[3]; //arr是一个指向包含3个int元素的数组的指针变量
//-----------------各种类型的指针------------------------------
int *p_int; //指向int类型变量的指针 
double *p_double; //指向idouble类型变量的指针 
struct Student *p_struct; //结构体类型的指针
int(*p_func)(int,int); //指向返回类型为int,有2个int形参的函数的指针
int(*p_arr)[3]; //指向含有3个int元素的数组的指针
int **p_pointer; //指向 一个整形变量指针的指针

 

取地址

 

既然有了指针变量,那就得让他保存其它变量的地址,使用& 运算符取得一个变量的地址。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int add(int a , int b){    return a + b;}
int main(void){ int num = 97; float score = 10.00F; int arr[3] = {1,2,3};
//-----------------------
int* p_num = &num; float* p_score = &score; int (*p_arr)[3] = &arr; int (*fp_add)(int ,int ) = add; //p_add是指向函数add的函数指针 return 0;}

 

特殊的情况,他们并不一定需要使用&取地址:

 

  • 数组名的值就是这个数组的第一个元素的地址。

  • 函数名的值就是这个函数的地址。

  • 字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int add(int a , int b){    return a + b;}int main(void){    int arr[3] = {1,2,3};    //-----------------------    int* p_first = arr;    int (*fp_add)(int ,int )  =  add;    const char* msg = "Hello world";    return 0;}

 

解地址

 

我们需要一个数据的指针变量干什么?

当然使用通过它来操作(读/写)它指向的数据啦。

对一个指针解地址,就可以取到这个内存数据,解地址的写法,就是在指针的前面加一个*号。

 

解指针的实质是:从指针指向的内存块中取出这个内存数据。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(void){    int age = 19;    int*p_age = &age;    *p_age  = 20;  //通过指针修改指向的内存数据
printf("age = %d",*p_age); //通过指针读取指向的内存数据 printf("age = %d",age);
return 0;}

 

指针之间的赋值

 

指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。

指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。

 

 

  •  
  •  
  •  
  •  
int *p1  = & num;int *p3 = p1;
//通过指针 p1 、 p3 都可以对内存数据 num 进行读写,如果2个函数分别使用了p1 和p3,那么这2个函数就共享了数据num。

 

对 C 语言指针最详尽的讲解

 

空指针

 

指向空,或者说不指向任何东西。

在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是 ((void*)0) ,  在C++中,NULL实质是0。

 

换种说法:任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。

 

下面代码摘自 stdlib.h

 

  •  
  •  
  •  
  •  
  •  
#ifdef __cplusplus     #define NULL    0#else         #define NULL    ((void *)0)#endif

 

坏指针

 

指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是坏指针。

不能对他们做解指针操作,否则程序会出现运行时错误,导致程序意外终止。

 

任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。

坏指针是造成C语言Bug的最频繁的原因之一。

 


下面的代码就是错误的示例。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
void opp(){     int *p = NULL;     *p = 10;      //Oops! 不能对NULL解地址}
void foo(){ int *p; *p = 10; //Oops! 不能对一个未知的地址解地址}
void bar(){ int *p = (int*)1000; *p =10; //Oops! 不能对一个可能不属于本程序的内存的地址的指针解地址}

 

 

指针的2个重要属性

 

指针也是一种数据,指针变量也是一种变量,因此指针 这种数据也符合前面变量和内存主题中的特性。

这里要强调2个属性:指针的类型,指针的值。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(void){    int num = 97;    int *p1  = &num;    char *p2 = (char*)(&num);
    printf("%d",*p1);     //输出  97    putchar(*p2);          //输出  a return 0;}

 

指针的值:很好理解,如上面的num 变量 ,其地址的值就是0028FF40 ,因此 p1的值就是0028FF40。

数据的地址用于在内存中定位和标识这个数据,因为任何2个内存不重叠的不同数据的地址都是不同的。

 

指针的类型:指针的类型决定了这个指针指向的内存的字节数并如何解释这些字节信息。

一般指针变量的类型要和它指向的数据的类型匹配。

 

由于num的地址是0028FF40,因此 p1 和 p2 的值都是0028FF40

 

*p1  :  将从地址0028FF40 开始解析,因为p1是int类型指针,int占4字节,因此向后连续取4个字节,并将这4个字节的二进制数据解析为一个整数 97。

 

*p2  :  将从地址0028FF40 开始解析,因为p2是char类型指针,char占1字节,因此向后连续取1个字节,并将这1个字节的二进制数据解析为一个字符,即’a’。

 

同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据。

 

void*类型指针 

 

由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址。

如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。

因为,编译器不允许直接对void*类型的指针做解指针操作。

 

结构体和指针

 

结构体指针有特殊的语法:-> 符号

如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
typedef struct{    char name[31];    int age;    float score;}Student;
int main(void){ Student stu = {"Bob" , 19, 98.0}; Student*ps = &stu;
ps->age = 20; ps->score = 99.0; printf("name:%s age:%d",ps->name,ps->age); return 0;}

 

数组和指针

 

1、数组名作为右值的时候,就是第一个元素的地址。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(void){    int arr[3] = {1,2,3};
int *p_first = arr;    printf("%d",*p_first);  //1 return 0;}

 

2、指向数组元素的指针 支持 递增 递减 运算。
(实质上所有指针都支持递增递减 运算 ,但只有在数组中使用才是有意义的)

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(void){    int arr[3] = {1,2,3};
int *p = arr; for(;p!=arr+3;p++){        printf("%d",*p);  } return 0;}

 

3、p= p+1 意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。

 

同一个数组中,元素的指针之间可以做减法运算,此时,指针之差等于下标之差。

 

4、p[n]    == *(p+n)

     p[n][m]  == *(  *(p+n)+ m )

 

5、当对数组名使用sizeof时,返回的是整个数组占用的内存字节数。当把数组名赋值给一个指针后,再对指针使用sizeof运算符,返回的是指针的大小。

 

这就是为什么将一个数组传递给一个函数时,需要另外用一个参数传递数组元素个数的原因了。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(void){    int arr[3] = {1,2,3};
int *p = arr;    printf("sizeof(arr)=%d",sizeof(arr));  //sizeof(arr)=12    printf("sizeof(p)=%d",sizeof(p));   //sizeof(p)=4
return 0;}

 

函数和指针

 

函数的参数和指针

 

C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。

这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
void change(int a){    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。}int main(void){    int age = 19;    change(age);    printf("age = %d",age);   // age = 19    return 0;}

 

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的。

但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

 

传递变量的指针可以轻松解决上述问题。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
void change(int *pa){    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,               //会直接去内存中找到age这个数据,然后把它增1}int main(void){    int age = 19;    change(&age);    printf("age = %d",age);   // age = 20    return 0;}

 

再来一个老生常谈的,用函数交换2个变量的值的例子:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#include <stdio.h>
void swap_bad(int a,int b);void swap_ok(int*pa,int*pb);
int main(){ int a = 5; int b = 3; swap_bad(a,b); //Can`t swap; swap_ok(&a,&b); //OK return 0;}
//错误的写法void swap_bad(int a,int b){ int t; t=a; a=b; b=t;}
//正确的写法:通过指针void swap_ok(int*pa,int*pb){ int t; t = *pa; *pa = *pb; *pb = t;}

 

对 C 语言指针最详尽的讲解

对 C 语言指针最详尽的讲解

 

有的时候,我们通过指针传递数据给函数不是为了在函数中改变他指向的对象。

相反,我们防止这个目标数据被改变。传递指针只是为了避免拷贝大型数据。

 

考虑一个结构体类型Student。我们通过show函数输出Student变量的数据。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
typedef struct{    char name[31];    int age;    float score;}Student;

//打印Student变量信息void show(const Student * ps){    printf("name:%s , age:%d , score:%.2f",ps->name,ps->age,ps->score);   }

 

我们只是在show函数中取读Student变量的信息,而不会去修改它,为了防止意外修改,我们使用了常量指针去约束。

另外我们为什么要使用指针而不是直接传递Student变量呢?

 

从定义的结构看出,Student变量的大小至少是39个字节,那么通过函数直接传递变量,实参赋值数据给形参需要拷贝至少39个字节的数据,极不高效。

而传递变量的指针却快很多,因为在同一个平台下,无论什么类型的指针大小都是固定的:X86指针4字节,X64指针8字节,远远比一个Student结构体变量小。

 

函数的指针

 

每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。

在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。

既然是存放在内存中,那么函数也是有自己的指针的。

 

C语言中,函数名作为右值时,就是这个函数的指针。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
void echo(const char *msg){    printf("%s",msg);}int main(void){    void(*p)(const char*) = echo;   //函数指针变量指向echo这个函数
p("Hello "); //通过函数的指针p调用函数,等价于echo("Hello ")    echo("World"); return 0;}

 

const和指针

 

const到底修饰谁?谁才是不变的?

 

如果const 后面是一个类型,则跳过最近的原子类型,修饰后面的数据。
(原子类型是不可再分割的类型,如int, short , char,以及typedef包装后的类型)

 

如果const后面就是一个数据,则直接修饰这个数据。

 

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
int main(){    int a = 1;
int const *p1 = &a; //const后面是*p1,实质是数据a,则修饰*p1,通过p1不能修改a的值 const int *p2 = &a; //const后面是int类型,则跳过int ,修饰*p2, 效果同上
    int *const p3 = NULL;      //const后面是数据p3。也就是指针p3本身是const .
    const int *const p4 = &a;  // 通过p4不能改变a 的值,同时p4本身也是 const    int const *const p5 = &a;  //效果同上
return 0;
} typedef int *pint_t;  //将 int* 类型 包装为 pint_t,则pint_t 现在是一个完整的原子类型
int main(){ int a = 1; const pint_t p1 = &a; //同样,const跳过类型pint_t,修饰p1,指针p1本身是const pint_t const p2 = &a; //const 直接修饰p,同上
    return 0;}

 

深拷贝和浅拷贝

 

如果2个程序单元(例如2个函数)是通过拷贝他们所共享的数据的指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。

如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互不受影响,则叫做深拷贝。

 对 C 语言指针最详尽的讲解

 

 

附加知识

 

指针和引用这个2个名词的区别。他们本质上来说是同样的东西。

指针常用在C语言中,而引用,则用于诸如Java,C#等 在语言层面封装了对指针的直接操作的编程语言中。

 

大端模式和小端模式

 

1) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。个人PC常用,Intel X86处理器是小端模式。

 

2) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

 

采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。

有些机器同时支持大端和小端模式,通过配置来设定实际的端模式。

 

假如 short类型占用2个字节,且存储的地址为0x30。

short a = 1;

 

如下图:

 

 对 C 语言指针最详尽的讲解

 

//测试机器使用的是否为小端模式。是,则返回true,否则返回false
//这个方法判别的依据就是:C语言中一个对象的地址就是这个对象占用的字节中,地址值最小的那个字节的地址。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
bool isSmallIndain(){      unsigned int val = 'A';      unsigned char* p = (unsigned char*)&val;  //C/C++:对于多字节数据,取地址是取的数据对象的第一个字节的地址,也就是数据的低地址
return *p == 'A';}

 

 

– EOF –

转自:https://mp.weixin.qq.com/s/IZgKdwJbH-5C2LoWOhZNaQ

五万字读懂c++

const作用

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
  3. 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。

const 的指针与引用

  • 指针
    • 指向常量的指针(pointer to const)
    • 自身是常量的指针(常量指针,const pointer)
  • 引用
    • 指向常量的引用(reference to const)
    • 没有 const reference,因为引用只是对象的别名,引用不是对象,不能用 const 修饰

(为了方便记忆可以想成)被 const 修饰(在 const 后面)的值不可改变,如下文使用例子中的 p2p3

使用

const 使用

// 类class A{private:    const int a;                // 常对象成员,可以使用初始化列表或者类内初始化public:    // 构造函数    A() : a(0) { };    A(int x) : a(x) { };        // 初始化列表    // const可用于对重载函数的区分    int getValue();             // 普通成员函数    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值};void function(){    // 对象    A b;                        // 普通对象,可以调用全部成员函数    const A a;                  // 常对象,只能调用常成员函数    const A *p = &a;            // 指针变量,指向常对象    const A &q = a;             // 指向常对象的引用    // 指针    char greeting[] = "Hello";    char* p1 = greeting;                // 指针变量,指向字符数组变量    const char* p2 = greeting;          // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)    char* const p3 = greeting;          // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)    const char* const p4 = greeting;    // 自身是常量的指针,指向字符数组常量}// 函数void function1(const int Var);           // 传递过来的参数在函数内不可变void function2(const char* Var);         // 参数指针所指内容为常量void function3(char* const Var);         // 参数指针为常量void function4(const int& Var);          // 引用参数在函数内为常量// 函数返回值const int function5();      // 返回一个常数const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

宏定义 #define 和 const 常量

宏定义 #defineconst 常量
宏定义,相当于字符替换常量声明
预处理器处理编译器处理
无类型安全检查有类型安全检查
不分配内存要分配内存
存储在代码段存储在数据段
可通过 #undef 取消不可取消

static

作用

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

this 指针

  1. this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。
  2. 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
  3. 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  4. this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
  5. this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  6. 在以下场景中,经常需要显式引用this
    指针:
    1. 为实现对象的链式引用;
    2. 为避免对同一对象进行赋值操作;
    3. 在实现一些数据结构时,如 list

inline 内联函数

特征

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

使用

inline 使用

// 声明1(加 inline,建议使用)inline int functionName(int first, int second,...);// 声明2(不加 inline)int functionName(int first, int second,...);// 定义inline int functionName(int first, int second,...) {/****/};// 类内定义,隐式内联class A {    int doA() { return 0; }         // 隐式内联}// 类外定义,需要显式内联class A {    int doA();}inline int A::doA() { return 0; }   // 需要显式内联

编译器对 inline 函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数(virtual)可以是内联函数(inline)吗?

Are “inline virtual” member functions ever actually “inlined”?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

虚函数内联使用

#include <iostream>  using namespace std;class Base{public: inline virtual void who() {  cout << "I am Base\n"; } virtual ~Base() {}};class Derived : public Base{public: inline void who()  // 不写inline时隐式内联 {  cout << "I am Derived\n"; }};int main(){ // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。  Base b; b.who(); // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。   Base *ptr = new Derived(); ptr->who(); // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。 delete ptr; ptr = nullptr; system("pause"); return 0;} 

volatile

volatile int i = 10; 
  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  • volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
  • const 可以是 volatile (如只读的状态寄存器)
  • 指针可以是 volatile

assert()

断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。

assert() 使用

#define NDEBUG          // 加上这行,则 assert 不可用#include <assert.h>assert( p != NULL );    // assert 不可用

sizeof()

  • sizeof 对数组,得到整个数组所占空间大小。
  • sizeof 对指针,得到指针本身所占空间大小。

#pragma pack(n)

设定结构体、联合以及类成员变量以 n 字节方式对齐

#pragma pack(n) 使用

#pragma pack(push)  // 保存对齐状态#pragma pack(4)     // 设定为 4 字节对齐struct test{    char m1;    double m4;    int m3;};#pragma pack(pop)   // 恢复对齐状态

位域

Bit mode: 2;    // mode 占 2 位

类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。

  • 位域在内存中的布局是与机器有关的
  • 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定
  • 取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域

extern “C”

  • 被 extern 限定的函数或变量是 extern 类型的
  • 被 extern "C" 修饰的变量和函数是按照 C 语言方式编译和链接的

extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

extern “C” 使用

#ifdef __cplusplusextern "C" {#endifvoid *memset(void *, int, size_t);#ifdef __cplusplus}#endif

struct 和 typedef struct

C 中

// ctypedef struct Student {    int age; } S;

等价于

// cstruct Student {     int age; };typedef struct Student S;

此时 S 等价于 struct Student,但两个标识符名称空间不相同。

另外还可以定义与 struct Student 不冲突的 void Student() {}

C++ 中

由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。

一、如果在类标识符空间定义了 struct Student {...};,使用 Student me; 时,编译器将搜索全局标识符表,Student 未找到,则在类标识符内搜索。

即表现为可以使用 Student 也可以使用 struct Student,如下:

// cppstruct Student {     int age; };void f( Student me );       // 正确,"struct" 关键字可省略

二、若定义了与 Student 同名函数之后,则 Student 只代表函数,不代表结构体,如下:

typedef struct Student {     int age; } S;void Student() {}           // 正确,定义后 "Student" 只代表此函数//void S() {}               // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名int main() {    Student();     struct Student me;      // 或者 "S me";    return 0;}

C++ 中 struct 和 class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

区别

  • 最本质的一个区别就是默认的访问控制
    1. 默认的继承访问权限。struct 是 public 的,class 是 private 的。
    2. struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

union 联合

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:

  • 默认访问控制符为 public
  • 可以含有构造函数、析构函数
  • 不能含有引用类型的成员
  • 不能继承自其他类,不能作为基类
  • 不能含有虚函数
  • 匿名 union 在定义所在作用域可直接访问 union 成员
  • 匿名 union 不能包含 protected 成员或 private 成员
  • 全局匿名联合必须是静态(static)的

union 使用

#include<iostream>union UnionTest {    UnionTest() : i(10) {};    int i;    double d;};static union {    int i;    double d;};int main() {    UnionTest u;    union {        int i;        double d;    };    std::cout << u.i << std::endl;  // 输出 UnionTest 联合的 10    ::i = 20;    std::cout << ::i << std::endl;  // 输出全局静态匿名联合的 20    i = 30;    std::cout << i << std::endl;    // 输出局部匿名联合的 30    return 0;}

C 实现 C++ 类

C 实现 C++ 的面向对象特性(封装、继承、多态)

  • 封装:使用函数指针把属性与方法封装到结构体中
  • 继承:结构体嵌套
  • 多态:父类与子类方法的函数指针不同

[Can you write object-oriented code in C? closed]

explicit(显式)关键字

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外

explicit 使用

struct A{ A(int) { } operator bool() const { return true; }};struct B{ explicit B(int) {} explicit operator bool() const { return true; }};void doA(A a) {}void doB(B b) {}int main(){ A a1(1);  // OK:直接初始化 A a2 = 1;  // OK:复制初始化 A a3{ 1 };  // OK:直接列表初始化 A a4 = { 1 };  // OK:复制列表初始化 A a5 = (A)1;  // OK:允许 static_cast 的显式转换  doA(1);   // OK:允许从 int 到 A 的隐式转换 if (a1);  // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a6(a1);  // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a7 = a1;  // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换 bool a8 = static_cast<bool>(a1);  // OK :static_cast 进行直接初始化 B b1(1);  // OK:直接初始化 B b2 = 1;  // 错误:被 explicit 修饰构造函数的对象不可以复制初始化 B b3{ 1 };  // OK:直接列表初始化 B b4 = { 1 };  // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化 B b5 = (B)1;  // OK:允许 static_cast 的显式转换 doB(1);   // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换 if (b1);  // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b6(b1);  // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b7 = b1;  // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换 bool b8 = static_cast<bool>(b1);  // OK:static_cast 进行直接初始化 return 0;}

friend 友元类和友元函数

  • 能访问私有成员
  • 破坏封装性
  • 友元关系不可传递
  • 友元关系的单向性
  • 友元声明的形式及数量不受限制

using

using 声明

一条 using 声明 语句一次只引入命名空间的一个成员。它使得我们可以清楚知道程序中所引用的到底是哪个名字。如:

using namespace_name::name;

构造函数的 using 声明

在 C++11 中,派生类能够重用其直接基类定义的构造函数。

class Derived : Base {public:    using Base::Base;    /* ... */};

如上 using 声明,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数。生成如下类型构造函数:

Derived(parms) : Base(args) { }

using 指示

using 指示 使得某个特定命名空间中所有名字都可见,这样我们就无需再为它们添加任何前缀限定符了。如:

using namespace_name name;

尽量少使用 using 指示 污染命名空间

一般说来,使用 using 命令比使用 using 编译命令更安全,这是由于它只导入了指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译命令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

using 使用

尽量少使用 using 指示

using namespace std;

应该多使用 using 声明

int x;std::cin >> x ;std::cout << x << std::endl;

或者

using std::cin;using std::cout;using std::endl;int x;cin >> x;cout << x << endl;

:: 范围解析运算符

分类

  1. 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
  2. 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
  3. 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

:: 使用

int count = 11;         // 全局(::)的 countclass A {public: static int count;   // 类 A 的 count(A::count)};int A::count = 21;void fun(){ int count = 31;     // 初始化局部的 count 为 31 count = 32;         // 设置局部的 count 的值为 32}int main() { ::count = 12;       // 测试 1:设置全局的 count 的值为 12 A::count = 22;      // 测试 2:设置类 A 的 count 为 22 fun();          // 测试 3 return 0;}

enum 枚举类型

限定作用域的枚举类型

enum class open_modes { input, output, append };

不限定作用域的枚举类型

enum color { red, yellow, green };enum { floatPrec = 6, doublePrec = 10 };

decltype

decltype 关键字用于检查实体的声明类型或表达式的类型及值分类。语法:

decltype ( expression )

decltype 使用

// 尾置返回允许我们在参数列表之后声明返回类型template <typename It>auto fcn(It beg, It end) -> decltype(*beg){    // 处理序列    return *beg;    // 返回序列中一个元素的引用}// 为了使用模板参数成员,必须用 typenametemplate <typename It>auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type{    // 处理序列    return *beg;    // 返回序列中一个元素的拷贝}

引用

左值引用

常规引用,一般表示对象的身份。

右值引用

右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。

右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  • 能够更简洁明确地定义泛型函数。

引用折叠

  • X& &X& &&X&& & 可折叠成 X&
  • X&& && 可折叠成 X&&

  • 宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对 “参数” 进行的是一对一的替换。

成员初始化列表

好处

  • 更高效:少了一次调用默认构造函数的过程。
  • 有些场合必须要用初始化列表:
    1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
    2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
    3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化

initializer_list 列表初始化

用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数.

initializer_list 使用

#include <iostream>#include <vector>#include <initializer_list> template <class T>struct S {    std::vector<T> v;    S(std::initializer_list<T> l) : v(l) {         std::cout << "constructed with a " << l.size() << "-element list\n";    }    void append(std::initializer_list<T> l) {        v.insert(v.end(), l.begin(), l.end());    }    std::pair<const T*, std::size_t> c_arr() const {        return {&v[0], v.size()};  // 在 return 语句中复制列表初始化                                   // 这不使用 std::initializer_list    }}; template <typename T>void templated_fn(T) {} int main(){    S<int> s = {1, 2, 3, 4, 5}; // 复制初始化    s.append({6, 7, 8});      // 函数调用中的列表初始化     std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";     for (auto n : s.v)        std::cout << n << ' ';    std::cout << '\n';     std::cout << "Range-for over brace-init-list: \n";     for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作        std::cout << x << ' ';    std::cout << '\n';     auto al = {10, 11, 12};   // auto 的特殊规则     std::cout << "The list bound to auto has size() = " << al.size() << '\n'; //    templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,                             // 它无类型,故 T 无法推导    templated_fn<std::initializer_list<int>>({1, 2, 3}); // OK    templated_fn<std::vector<int>>({1, 2, 3});           // 也 OK}

面向对象

面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

面向对象三大特征 —— 封装、继承、多态

封装

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。

  • public 成员:可以被任意实体访问
  • protected 成员:只允许被子类及本类的成员函数访问
  • private 成员:只允许被本类的成员函数、友元类或友元函数访问

继承

  • 基类(父类)——> 派生类(子类)

多态

  • 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。
  • 多态是以封装和继承为基础的。
  • C++ 多态分类及实现:
    1. 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
    2. 子类型多态(Subtype Polymorphism,运行期):虚函数
    3. 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
    4. 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

The Four Polymorphisms in C++

静态多态(编译期/早绑定)

函数重载

class A{public:    void do(int a);    void do(int a, int b);};

动态多态(运行期期/晚绑定)

  • 虚函数:用 virtual 修饰成员函数,使其成为虚函数
  • 动态绑定:当使用基类的引用或指针调用一个虚函数时将发生动态绑定

注意:

  • 可以将派生类的对象赋值给基类的指针或引用,反之不可
  • 普通函数(非类成员函数)不能是虚函数
  • 静态函数(static)不能是虚函数
  • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
  • 内联函数不能是表现多态性时的虚函数,解释见:虚函数(virtual)可以是内联函数(inline)吗?

动态多态使用

class Shape                     // 形状类{public:    virtual double calcArea()    {        ...    }    virtual ~Shape();};class Circle : public Shape     // 圆形类{public:    virtual double calcArea();    ...};class Rect : public Shape       // 矩形类{public:    virtual double calcArea();    ...};int main(){    Shape * shape1 = new Circle(4.0);    Shape * shape2 = new Rect(5.0, 6.0);    shape1->calcArea();         // 调用圆形类里面的方法    shape2->calcArea();         // 调用矩形类里面的方法    delete shape1;    shape1 = nullptr;    delete shape2;    shape2 = nullptr;    return 0;}

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

虚析构函数使用

class Shape{public:    Shape();                    // 构造函数不能是虚函数    virtual double calcArea();    virtual ~Shape();           // 虚析构函数};class Circle : public Shape     // 圆形类{public:    virtual double calcArea();    ...};int main(){    Shape * shape1 = new Circle(4.0);    shape1->calcArea();        delete shape1;  // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。    shape1 = NULL;    return 0;}

纯虚函数

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

virtual int A() = 0;

虚函数、纯虚函数

  • 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖(override),这样的话,编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
  • 虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
  • 虚函数的类用于 “实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
  • 带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类,具体见下文虚继承。

CSDN . C++ 中的虚函数、纯虚函数区别和联系

虚函数指针、虚函数表

  • 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
  • 虚函数表:在程序只读数据段(.rodata section,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现

虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。

底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

虚继承、虚函数

  • 相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同之处:
    • 虚函数不占用存储空间
    • 虚函数表存储的是虚函数地址
    • 虚基类依旧存在继承类中,只占用存储空间
    • 虚基类表存储的是虚基类相对直接继承类的偏移
    • 虚继承
    • 虚函数

模板类、成员模板、虚函数

  • 模板类中可以使用虚函数
  • 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数

抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
    • 所有成员都是 public
    • 没有定义任何构造函数
    • 没有类内初始化
    • 没有基类,也没有 virtual 函数

内存分配和管理

malloc、calloc、realloc、alloca

  1. malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
  2. calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
  3. realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
  4. alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。

malloc、free

用于分配、释放内存

malloc、free 使用

申请内存,确认是否申请成功

char *str = (char*) malloc(100);assert(str != nullptr);

释放内存后指针置空

free(p); p = nullptr;

new、delete

  1. new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
  2. delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
  3. new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

new、delete 使用

申请内存,确认是否申请成功

int main(){    T* t = new T();     // 先内存分配 ,再构造函数    delete t;           // 先析构函数,再内存释放    return 0;}

定位 new

定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。

new (place_address) typenew (place_address) type (initializers)new (place_address) type [size]new (place_address) type [size] { braced initializer list }
  • place_address 是个指针
  • initializers 提供一个(可能为空的)以逗号分隔的初始值列表

delete this 合法吗?

Is it legal (and moral) for a member function to say delete this?

合法,但:

  1. 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
  2. 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
  3. 必须保证成员函数的 delete this后面没有调用 this 了
  4. 必须保证 delete this 后没有人使用了

如何定义一个只能在堆上(栈上)生成对象的类?

如何定义一个只能在堆上(栈上)生成对象的类?

只能在堆上

方法:将析构函数设置为私有

原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

只能在栈上

方法:将 new 和 delete 重载为私有

原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

智能指针

C++ 标准库(STL)中

头文件:#include <memory>

C++ 98

std::auto_ptr<std::string> ps (new std::string(str));

C++ 11

  1. shared_ptr
  2. unique_ptr
  3. weak_ptr
  4. auto_ptr(被 C++11 弃用)
  • Class shared_ptr 实现共享式拥有(shared ownership)概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。
  • Class unique_ptr 实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。
shared_ptr

多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。

  • 支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁
weak_ptr

weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。

  • 可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题
unique_ptr

unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。

  • unique_ptr 用于取代 auto_ptr
auto_ptr

被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。

auto_ptr 与 unique_ptr 比较
  • auto_ptr 可以赋值拷贝,复制拷贝后所有权转移;unqiue_ptr 无拷贝赋值语义,但实现了move 语义;
  • auto_ptr 对象不能管理数组(析构调用 delete),unique_ptr 可以管理数组(析构调用 delete[] );

强制类型转换运算符

MSDN . 强制转换运算符

static_cast

  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

向上转换是一种隐式转换。

dynamic_cast

  • 用于多态类型的转换
  • 执行行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
  • 可以在整个类层次结构中移动指针,包括向上转换、向下转换

const_cast

  • 用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )

reinterpret_cast

  • 用于位的简单重新解释
  • 滥用 reinterpret_cast 运算符可能很容易带来风险。除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
  • 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
  • 也允许将任何整数类型转换为任何指针类型以及反向转换。
  • reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
  • reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。

bad_cast

  • 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。

bad_cast 使用

try {      Circle& ref_circle = dynamic_cast<Circle&>(ref_shape);   }  catch (bad_cast b) {      cout << "Caught: " << b.what();  } 

运行时类型信息 (RTTI)

dynamic_cast

  • 用于多态类型的转换

typeid

  • typeid 运算符允许在运行时确定对象的类型
  • type_id 返回一个 type_info 对象的引用
  • 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
  • 只能获取对象的实际类型

type_info

  • type_info 类描述编译器在程序中生成的类型信息。此类的对象可以有效存储指向类型的名称的指针。type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值。类型的编码规则和排列顺序是未指定的,并且可能因程序而异。
  • 头文件:typeinfo

typeid、type_info 使用

#include <iostream>using namespace std;class Flyable                       // 能飞的{public:    virtual void takeoff() = 0;     // 起飞    virtual void land() = 0;        // 降落};class Bird : public Flyable         // 鸟{public:    void foraging() {...}           // 觅食    virtual void takeoff() {...}    virtual void land() {...}    virtual ~Bird(){}};class Plane : public Flyable        // 飞机{public:    void carry() {...}              // 运输    virtual void takeoff() {...}    virtual void land() {...}};class type_info{public:    const char* name() const;    bool operator == (const type_info & rhs) const;    bool operator != (const type_info & rhs) const;    int before(const type_info & rhs) const;    virtual ~type_info();private:    ...};void doSomething(Flyable *obj)                 // 做些事情{    obj->takeoff();    cout << typeid(*obj).name() << endl;        // 输出传入对象类型("class Bird" or "class Plane")    if(typeid(*obj) == typeid(Bird))            // 判断对象类型    {        Bird *bird = dynamic_cast<Bird *>(obj); // 对象转化        bird->foraging();    }    obj->land();}int main(){ Bird *b = new Bird(); doSomething(b); delete b; b = nullptr; return 0;}

⭐️ Effective

Effective C++

  1. 视 C++ 为一个语言联邦(C、Object-Oriented C++、Template C++、STL)
  2. 宁可以编译器替换预处理器(尽量以 constenuminline 替换 #define
  3. 尽可能使用 const
  4. 确定对象被使用前已先被初始化(构造时赋值(copy 构造函数)比 default 构造后赋值(copy assignment)效率高)
  5. 了解 C++ 默默编写并调用哪些函数(编译器暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符、析构函数)
  6. 若不想使用编译器自动生成的函数,就应该明确拒绝(将不想使用的成员函数声明为 private,并且不予实现)
  7. 为多态基类声明 virtual 析构函数(如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数)
  8. 别让异常逃离析构函数(析构函数应该吞下不传播异常,或者结束程序,而不是吐出异常;如果要处理异常应该在非析构的普通函数处理)
  9. 绝不在构造和析构过程中调用 virtual 函数(因为这类调用从不下降至 derived class)
  10. 令 operator= 返回一个 reference to *this (用于连锁赋值)
  11. 在 operator= 中处理 “自我赋值”
  12. 赋值对象时应确保复制 “对象内的所有成员变量” 及 “所有 base class 成分”(调用基类复制构造函数)
  13. 以对象管理资源(资源在构造函数获得,在析构函数释放,建议使用智能指针,资源取得时机便是初始化时机(Resource Acquisition Is Initialization,RAII))
  14. 在资源管理类中小心 copying 行为(普遍的 RAII class copying 行为是:抑制 copying、引用计数、深度拷贝、转移底部资源拥有权(类似 auto_ptr))
  15. 在资源管理类中提供对原始资源(raw resources)的访问(对原始资源的访问可能经过显式转换或隐式转换,一般而言显示转换比较安全,隐式转换对客户比较方便)
  16. 成对使用 new 和 delete 时要采取相同形式(new 中使用 [] 则 delete []new 中不使用 [] 则 delete
  17. 以独立语句将 newed 对象存储于(置入)智能指针(如果不这样做,可能会因为编译器优化,导致难以察觉的资源泄漏)
  18. 让接口容易被正确使用,不易被误用(促进正常使用的办法:接口的一致性、内置类型的行为兼容;阻止误用的办法:建立新类型,限制类型上的操作,约束对象值、消除客户的资源管理责任)
  19. 设计 class 犹如设计 type,需要考虑对象创建、销毁、初始化、赋值、值传递、合法值、继承关系、转换、一般化等等。
  20. 宁以 pass-by-reference-to-const 替换 pass-by-value (前者通常更高效、避免切割问题(slicing problem),但不适用于内置类型、STL迭代器、函数对象)
  21. 必须返回对象时,别妄想返回其 reference(绝不返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。)
  22. 将成员变量声明为 private(为了封装、一致性、对其读写精确控制等)
  23. 宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
  24. 若所有参数(包括被this指针所指的那个隐喻参数)皆须要类型转换,请为此采用 non-member 函数
  25. 考虑写一个不抛异常的 swap 函数
  26. 尽可能延后变量定义式的出现时间(可增加程序清晰度并改善程序效率)
  27. 尽量少做转型动作(旧式:(T)expressionT(expression);新式:const_cast<T>(expression)dynamic_cast<T>(expression)reinterpret_cast<T>(expression)static_cast<T>(expression)、;尽量避免转型、注重效率避免 dynamic_casts、尽量设计成无需转型、可把转型封装成函数、宁可用新式转型)
  28. 避免使用 handles(包括 引用、指针、迭代器)指向对象内部(以增加封装性、使 const 成员函数的行为更像 const、降低 “虚吊号码牌”(dangling handles,如悬空指针等)的可能性)
  29. 为 “异常安全” 而努力是值得的(异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或允许任何数据结构败坏,分为三种可能的保证:基本型、强列型、不抛异常型)
  30. 透彻了解 inlining 的里里外外(inlining 在大多数 C++ 程序中是编译期的行为;inline 函数是否真正 inline,取决于编译器;大部分编译器拒绝太过复杂(如带有循环或递归)的函数 inlining,而所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空;inline 造成的代码膨胀可能带来效率损失;inline 函数无法随着程序库的升级而升级)
  31. 将文件间的编译依存关系降至最低(如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects;如果能够,尽量以 class 声明式替换 class 定义式;为声明式和定义式提供不同的头文件)
  32. 确定你的 public 继承塑模出 is-a(是一种)关系(适用于 base classes 身上的每一件事情一定适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象)
  33. 避免遮掩继承而来的名字(可使用 using 声明式或转交函数(forwarding functions)来让被遮掩的名字再见天日)
  34. 区分接口继承和实现继承(在 public 继承之下,derived classes 总是继承 base class 的接口;pure virtual 函数只具体指定接口继承;非纯 impure virtual 函数具体指定接口继承及缺省实现继承;non-virtual 函数具体指定接口继承以及强制性实现继承)
  35. 考虑 virtual 函数以外的其他选择(如 Template Method 设计模式的 non-virtual interface(NVI)手法,将 virtual 函数替换为 “函数指针成员变量”,以 tr1::function 成员变量替换 virtual 函数,将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数)
  36. 绝不重新定义继承而来的 non-virtual 函数
  37. 绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定(statically bound),而 virtual 函数却是动态绑定(dynamically bound)
  38. 通过复合塑模 has-a(有一个)或 “根据某物实现出”(在应用域(application domain),复合意味 has-a(有一个);在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出))
  39. 明智而审慎地使用 private 继承(private 继承意味着 is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,当 derived class 需要访问 protected base class 的成员,或需要重新定义继承而来的时候 virtual 函数,或需要 empty base 最优化时,才使用 private 继承)
  40. 明智而审慎地使用多重继承(多继承比单一继承复杂,可能导致新的歧义性,以及对 virtual 继承的需要,但确有正当用途,如 “public 继承某个 interface class” 和 “private 继承某个协助实现的 class”;virtual 继承可解决多继承下菱形继承的二义性问题,但会增加大小、速度、初始化及赋值的复杂度等等成本)
  41. 了解隐式接口和编译期多态(class 和 templates 都支持接口(interfaces)和多态(polymorphism);class 的接口是以签名为中心的显式的(explicit),多态则是通过 virtual 函数发生于运行期;template 的接口是奠基于有效表达式的隐式的(implicit),多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期)
  42. 了解 typename 的双重意义(声明 template 类型参数是,前缀关键字 class 和 typename 的意义完全相同;请使用关键字 typename 标识嵌套从属类型名称,但不得在基类列(base class lists)或成员初值列(member initialization list)内以它作为 base class 修饰符)
  43. 学习处理模板化基类内的名称(可在 derived class templates 内通过 this-> 指涉 base class templates 内的成员名称,或藉由一个明白写出的 “base class 资格修饰符” 完成)
  44. 将与参数无关的代码抽离 templates(因类型模板参数(non-type template parameters)而造成代码膨胀往往可以通过函数参数或 class 成员变量替换 template 参数来消除;因类型参数(type parameters)而造成的代码膨胀往往可以通过让带有完全相同二进制表述(binary representations)的实现类型(instantiation types)共享实现码)
  45. 运用成员函数模板接受所有兼容类型(请使用成员函数模板(member function templates)生成 “可接受所有兼容类型” 的函数;声明 member templates 用于 “泛化 copy 构造” 或 “泛化 assignment 操作” 时还需要声明正常的 copy 构造函数和 copy assignment 操作符)
  46. 需要类型转换时请为模板定义非成员函数(当我们编写一个 class template,而它所提供之 “与此 template 相关的” 函数支持 “所有参数之隐式类型转换” 时,请将那些函数定义为 “class template 内部的 friend 函数”)
  47. 请使用 traits classes 表现类型信息(traits classes 通过 templates 和 “templates 特化” 使得 “类型相关信息” 在编译期可用,通过重载技术(overloading)实现在编译期对类型执行 if…else 测试)
  48. 认识 template 元编程(模板元编程(TMP,template metaprogramming)可将工作由运行期移往编译期,因此得以实现早期错误侦测和更高的执行效率;TMP 可被用来生成 “给予政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码)
  49. 了解 new-handler 的行为(set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常)
  50. 了解 new 和 delete 的合理替换时机(为了检测运用错误、收集动态分配内存之使用统计信息、增加分配和归还速度、降低缺省内存管理器带来的空间额外开销、弥补缺省分配器中的非最佳齐位、将相关对象成簇集中、获得非传统的行为)
  51. 编写 new 和 delete 时需固守常规(operator new 应该内涵一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用 new-handler,它也应该有能力处理 0 bytes 申请,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”;operator delete 应该在收到 null 指针时不做任何事,class 专属版本则还应该处理 “比正确大小更大的(错误)申请”)
  52. 写了 placement new 也要写 placement delete(当你写一个 placement operator new,请确定也写出了对应的 placement operator delete,否则可能会发生隐微而时断时续的内存泄漏;当你声明 placement new 和 placement delete,请确定不要无意识(非故意)地遮掩了它们地正常版本)
  53. 不要轻忽编译器的警告
  54. 让自己熟悉包括 TR1 在内的标准程序库(TR1,C++ Technical Report 1,C++11 标准的草稿文件)
  55. 让自己熟悉 Boost(准标准库)

More Effective c++

  1. 仔细区别 pointers 和 references(当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由 pointers 达成,你就应该选择 references;任何其他时候,请采用 pointers)
  2. 最好使用 C++ 转型操作符(static_castconst_castdynamic_castreinterpret_cast
  3. 绝不要以多态(polymorphically)方式处理数组(多态(polymorphism)和指针算术不能混用;数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用)
  4. 非必要不提供 default constructor(避免对象中的字段被无意义地初始化)
  5. 对定制的 “类型转换函数” 保持警觉(单自变量 constructors 可通过简易法(explicit 关键字)或代理类(proxy classes)来避免编译器误用;隐式类型转换操作符可改为显式的 member function 来避免非预期行为)
  6. 区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式(前置式累加后取出,返回一个 reference;后置式取出后累加,返回一个 const 对象;处理用户定制类型时,应该尽可能使用前置式 increment;后置式的实现应以其前置式兄弟为基础)
  7. 千万不要重载 &&|| 和 , 操作符(&& 与 || 的重载会用 “函数调用语义” 取代 “骤死式语义”;, 的重载导致不能保证左侧表达式一定比右侧表达式更早被评估)
  8. 了解各种不同意义的 new 和 delete(new operatoroperator newplacement newoperator new[]delete operatoroperator deletedestructoroperator delete[]
  9. 利用 destructors 避免泄漏资源(在 destructors 释放资源可以避免异常时的资源泄漏)
  10. 在 constructors 内阻止资源泄漏(由于 C++ 只会析构已构造完成的对象,因此在构造函数可以使用 try…catch 或者 auto_ptr(以及与之相似的 classes) 处理异常时资源泄露问题)
  11. 禁止异常流出 destructors 之外(原因:一、避免 terminate 函数在 exception 传播过程的栈展开(stack-unwinding)机制种被调用;二、协助确保 destructors 完成其应该完成的所有事情)
  12. 了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异(第一,exception objects 总是会被复制(by pointer 除外),如果以 by value 方式捕捉甚至被复制两次,而传递给函数参数的对象则不一定得复制;第二,“被抛出成为 exceptions” 的对象,其被允许的类型转换动作比 “被传递到函数去” 的对象少;第三,catch 子句以其 “出现于源代码的顺序” 被编译器检验对比,其中第一个匹配成功者便执行,而调用一个虚函数,被选中执行的是那个 “与对象类型最佳吻合” 的函数)
  13. 以 by reference 方式捕获 exceptions(可避免对象删除问题、exception objects 的切割问题,可保留捕捉标准 exceptions 的能力,可约束 exception object 需要复制的次数)
  14. 明智运用 exception specifications(exception specifications 对 “函数希望抛出什么样的 exceptions” 提供了卓越的说明;也有一些缺点,包括编译器只对它们做局部性检验而很容易不经意地违反,与可能会妨碍更上层的 exception 处理函数处理未预期的 exceptions)
  15. 了解异常处理的成本(粗略估计,如果使用 try 语句块,代码大约整体膨胀 5%-10%,执行速度亦大约下降这个数;因此请将你对 try 语句块和 exception specifications 的使用限制于非用不可的地点,并且在真正异常的情况下才抛出 exceptions)
  16. 谨记 80-20 法则(软件的整体性能几乎总是由其构成要素(代码)的一小部分决定的,可使用程序分析器(program profiler)识别出消耗资源的代码)
  17. 考虑使用 lazy evaluation(缓式评估)(可应用于:Reference Counting(引用计数)来避免非必要的对象复制、区分 operator[] 的读和写动作来做不同的事情、Lazy Fetching(缓式取出)来避免非必要的数据库读取动作、Lazy Expression Evaluation(表达式缓评估)来避免非必要的数值计算动作)
  18. 分期摊还预期的计算成本(当你必须支持某些运算而其结构几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation(超急评估)可以改善程序效率)

转自:https://mp.weixin.qq.com/s/GOk0jlbJPl9NNtXmdLz9WQ

Github上「清华大学计算机系课程攻略」

经常有大一的同学或者非科班转码的同学在后台问我,计算机都要学哪些课程,有什么先后顺序。

有类似疑问的小伙伴,可以参考Github上「清华大学计算机系课程攻略

后悔大学没看到这个 Github 仓库!
img

里面包含了清华CS专业,大一到大四的课程以及推荐书籍资源等,链接如下:

https://github.com/PKUanonym/REKCARC-TSC-UHTgithub.com

截取一些课程目录:

大一上:

后悔大学没看到这个 Github 仓库!

大一下:

后悔大学没看到这个 Github 仓库!

大二上:

后悔大学没看到这个 Github 仓库!

大二下:

后悔大学没看到这个 Github 仓库!

大三,专业课明显多起来了:

后悔大学没看到这个 Github 仓库!
后悔大学没看到这个 Github 仓库!

大四主要是实践、实习、毕设,课不多:

后悔大学没看到这个 Github 仓库!

不过说实话,这份攻略对于我们大部分同学来说都只能参考,课程量有点大,时间不充足的话,很难完全跟下来。

但是我觉得这里面大三下的课程非常棒:

  • 操作系统
  • 计算机系统
  • 数值分析
  • 数字图像
  • 存储技术
  • 搜索引擎技术
  • 模式识别
  • 机器学习

可以说大三下的这些课程铺垫非常不错,不管以后走分布式存储、搜索引擎等基础架构方向还是机器学习等算法方向,亦或是游戏引擎、图形处理等方向,都算是入门了。

但是说实话,清华这份攻略好是好,就是太多了,对于时间没有那么充裕的同学,想要一个更加精简的 List,可以参考网易云课堂上的计算机课程图:

后悔大学没看到这个 Github 仓库!

我把几门我觉得基本必学的课程用红线框起来了,大家可以作为参考。

另外,学习这些课程的时候,一定要多写代码、做一些Lab,建议把操作系统、计网、csapp这些课程 lab做完(国内有些学校可能没有),可以去国外找计算机四大名校的Lab。

部分学校最大的问题就是 OS、网络、数据库等 CS 核心课程上成了文科!

缺乏一些动手实践 Lab,大部分都是写实验报告,就是不写代码。

为什么美帝在数据库、操作系统、各种工业软件工程能力如此强悍?

**我觉得和他们 CS 强校的教育模式有关,**国外,尤其是美国,很多高校 CS 教育极其注重实践,国内也就少数不错的高校有实践教学,其它都是念 PPT,课后作业就是做 Word 项目。。。。

就拿 MIT 来说,6.830 Database Systems: 这是一门数据库系统的一门核心课程。前半部分比较基础的数据库的知识,后半段主要在讲Distributed Databases 的东西

它们的实验是什么?

是写个小型关系型数据库,比如 lab1实现数据库基本的存储逻辑结构,具体包括:Tuple,TupleDesc,HeapPage,HeapFile,SeqScan,BufferPool等。

我们数据库教什么?

教 SQL 怎么写,教数据库原理(这个还算好的)

大多数高校实验是什么?

用 Java 使用 JDBC 连接 MySQL 写个应用。。。

国外是如何教计算机网络的?

实现  TCP 协议栈~

比如斯坦福 CS 144,CS144 Lab Assignments – 手写TCP – LAB4

后悔大学没看到这个 Github 仓库!

我们是怎么学习的?

记住 TCP 三次握手、四次挥手、记住 DNS 使用 UDP 协议….

我们大作业是什么?

有些是 Word 写作业。。。有些是让你用一下 socket api 写个网络程序。。。这是计算机网络原理吗?

操作系统比较典型的是 MIT 6.828,现在改名了。

直接让你基于一个 xv6 去实现一些模块,比如文件系统、多线程。

我们呢?

记住进程和线程区别、记住虚拟内存的各种组关联….

当然也有一些学校会有实验,比如清华 ucore、哈工大 linux0.11。

这就是为什么美国高校有很强的编码能力、造轮子的能力,因为他们的 CS 学生成长环境就是这样的。

CS 专业区别于其它专业一个很大特点就是:

  • 工作后的内容是和专业所学的内容强相关的。

比如你学了数据结构、编译原理、操作系统、计算机网络,如果你从事的是研发岗,那一定离不开这些知识。

  • 主要靠自学

不管是科班还是非科班,想要快速持续的提高技术水平,就得靠自己去钻,尤其离不开自学。

知乎上其实很多问科班和非科班的差别在哪,其实我一直想说,你给自己充足时间去把科班的内容学习一遍,到底还能差在哪呢?

可能唯一差别就是少了一个 计算机学士学位。

也有人把这种自学出家的叫做民科,当然没有任何的讽刺意思哈。

那么计算机专业同学该如何自学呢?

其实看着很多,概况起来就是(下面只涉及CS专业课):

  1. 计算机导论 + 一门编程入门语言
  2. 算法与数据结构
  3. 操作系统
  4. 计算机网络
  5. 数据库系统
  6. 特定领域知识,如:计算机图形学、信息安全、System方向、分布式

学习的途径就是:

多看国内外 CS 名校的一些开放课程 + 看经典的书 + 多写代码!!!

毕竟现在MOOC、Udemy、B站上学习的资源都是很丰富的。

比如:[CS经典课程][https://mp.weixin.qq.com/s/bkLpjLxqQHaBTwCiz3VwBA]

简单列举几个学科:

一、计算机导论

首先建议从计算机导论课程开始,推荐下面这些课程:

  • Harvard的CS50  CS50: Introduction to Computer Science
  • Berkeley的CS61A  CS 61A: Structure and Interpretation of Computer Programs
  • MIT的6.001  mit-6.001

二、C 语言

随后建议学习一门语言,可以是 C、Java、或 Python,我推荐 C语言(当然,也可以是Python!这不是重点,重点是要多去写,入门时提高对编程的兴趣),提到C语言,我这里推荐国内浙大翁凯老师的课,看过的都说好~

分为两门,第一门是面向高考结束想提前自学一点编程的,叫大学先修课:

  • C语言程序设计CAP-大学先修课

虽然叫先修课,但是覆盖了C语言的主要知识点,也适合大一新生~

第二门是

  • C语言程序设计进阶

会带你用C语言完成一些有趣的项目,比如一些图形界面小游戏,先修课学习 C 语言语法基础,进阶课带你项目实操,搭配使用,你就是同学中的大神!

有了语言基础之后建议学数据结构与算法:

三、数据结构推荐:

  • Stanford CS106系列
  • CS106A: Programming Methodologies

四、算法推荐:

  • 6.046(进阶)  Design and Analysis of Algorithms – MIT
  • MIT的6.006  Introduction to Algorithms
  • Coursera上的Princeton课程
  • Berkeley的CS61A 和 CS61B

五、操作系统推荐:

  • CMU的15-213
  • Berkeley的CS162,

这两个都是有视频有lab的好课

还有一个非常经典的 MIT 6.828,附带一个xv6 lab

课程:6.828: Operating System Engineering

六、 组成原理、体系结构:

  1. MIT的6.004,
  2. CMU的15-213
  3. Berkeley的CS61C

七、计算机网络:

  • Stanford的CS144,lab 很有意思

新手快速自学的方法

一个原则,来自翁凯老师:

学计算机一定要有一个非常强大的心理状态,计算机的所有东西都是人做出来的,别人能想的出来,我也一定能想得出来,在计算机的世界里没有任何的黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全搞明白的

这里,也有一个问题,确实大部分的东西,只要你一直深挖下去,可能都能搞明白。

但是要注意时间成本,软件行业已经不是一般的复杂和巨大,任何一个领域的知识的复杂性都足够耗费掉我们一生的时间,所以一定要抓住主线,对于技术和知识,要学通用的、流行的,可以尝试面向面试学习。

“打破砂锅问到底”式的学习虽然精神可敬,但效率并不划算。

要在适当的层次上抽象出一层,并且认可这一层提供的接口,不去深究内部实现,了解原理即可,不必深究内部实现。

比如学习 HTTP,那么就先认可 TCP 提供的稳定可靠传输,而不继续深挖 TCP 的内容。

新手,一定不要一直看书,保持看书的时间不超过 50%。

  1. 看书学习基本的理论
  2. 编程练习
  3. 有了新领悟,继续看书

反复的循环。

转自:https://mp.weixin.qq.com/s/vmXKinw3FAEmwHltUmK2ew

两万字总结《C++ Primer》要点

对于想要入门C++的同学来说,《C++ Primer》是一本不能错过的入门书籍,它用平易近人的实例化教学激发学生的学习兴趣,帮助学生一步步走进C++的大门。在本文中,作者Jacen用两万多字总结了《C++ Primer 中文版(第五版)》1-16章的阅读要点,可以作为该书的阅读参考。注:原书更为详细,本文仅作学习交流使用。

第一章 开始

1.1 编写一个简单的C++程序

int main()
{
return 0;
}

每个C++程序都包含一个或多个函数,其中一个必须命名为main.

1.2 初识输入输出

对象用途
cin标准输入
cout标准输出
cerr标准错误
clog输出运行时的一般性消息

1.3 注释简介

两种:单行注释://界定符:/* 和 */

1.4 控制流

while;for;if;
第二章 变量和基本类型
P30-P71数据类型是程序的基础。C++语言支持广泛的数据类型。

基本内置类型

1.算术类型

类型最小尺寸
bool未定义
char8位
w_char_t16位
char16_t16位
char32_t32位
short16位
int16位
long32位
long long64位
float6位有效数字
double10位有效数字
long double10位有效数字

2.类型转换

不要混用符号类型和无符号类型。

变量

1.变量定义

(1)基本形式:类型说明符,随后紧跟着一个或者多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。(2)初始值在C++中,初始化和赋值是2个完全不同的操作。初始化的含义是创建变量的时候赋予一个初始值,而赋值的含义是把对象的当前值擦除,用一个新值来替代。两者区别很小。(3)列表初始化用花括号来初始化变量的方式,称为列表初始化。(4)默认初始化如果定义变量没有指定初始值,则变量被默认初始化。::: tip例外情况:定义在函数体内部的内置类型变量将不被初始化,其值未定义。建议初始化每个内置类型的变量。:::

2.变量声明和定义的关系

变量声明:规定了变量的类型和名字。变量定义:除声明之外,还需要申请存储空间。如果想声明一个变量,而非定义它,需要使用extern关键词。

extern int i;    // 声明i而非定义i
int j;           // 声明并定义j

::: tip变量只能被定义一次,但可以被多次声明。:::

3.名字的作用域

作用域:C++中大多数作用域都用花括号分隔。作用域中一旦声明了某个名字,它所嵌套的所有作用域都能访问该名字。同时,允许在内层作用域中重新定义外层作用域中有的名字。::: warning如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。:::

复合类型

定义:复合类型是基于其他类型定义的类型。

1.引用

引用:为对象起另外一个名字。::: warning引用必须被初始化。引用本身不是对象,所以不能定义引用的引用。引用要和绑定的对象严格匹配。引用类型的初始值,必须是一个对象。:::

2.指针

指针:本身就是一个对象。允许对指针赋值和拷贝。指针无须在定义的时候赋值。(1)利用指针访问对象如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象。(2)void* 指针

3.理解复合类型的声明

(1)指向指针的指针** 表示指向指针的指针*** 表示指向指针的指针的指针(2)指向指针的引用不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。

const限定符

定义:const用于定义一个变量,它的值不能被改变。const对象必须初始化。::: tip默认状态下,const对象仅在文件内有效。当多个文件出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。如果想让const变量在文件间共享,则使用extern修饰。:::(1)const的引用允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。一般,引用的类型必须与其所引用对象的类型一致,特殊情况是表达式。(2)指针和const弄清楚类型,可以从右边往左边阅读。(3)顶层consttop-level const 表示指针本身是个常量low-level const表示指针所指的对象是一个常量。(4)constexpr和常量表达式C++新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。

处理类型

类型别名

两种方法用于定义类型别名:(1)使用关键词typedef

typedef double wages; //wages是double的同义词
typedef wages *p; // p是double*的同义词

(2)别名声明

using SI = Sales_item;  // SI是Sales_item的同义词

auto类型说明符:让编译器通过初始值来推算变量的类型。decltype类型指示符:选择并返回操作符的数据类型。只得到类型,不实际计算表达式的值。

自定义数据结构

(1)类数据结构是把一组相关的数据元素组织起来,然后使用它们的策略和方法。类一般不定义在函数体内,为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。头文件通常包含那些被定义一次的实体。(2)预处理器

#ifndef SALES_DATA_H
#define SALES_DATA_H
#endif

一般把预处理变量的名字全部大写。

术语

空指针 :值为0的指针,空指针合法但是不指向任何对象。nullPtr是表示空指针的字面值常量。void*:可以指向任意非常量的指针类型,不能执行解引用操作。
第三章 字符串、向量和数组
P74-P118string表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。

命名空间的 using 声明

using namespace:name;

头文件不应包含using声明。

标准库类型 string

#include <string>
using namespace std;

(1)定义和初始化

string s1;
sting s2(s1);
string s3("value");
string s3 = "value";
string s4(n, 'c');

(2)string对象的操作

s.empty();      // 判空
s.size(); // 字符个数
s[n]; // s中第n个字符的引用
s1+s2; // s1和s2连接
<,<=,>,>= // 比较

::: warning标准局允许把字面值和字符串字面值转换成string对象。字面值和string是不同的类型。:::(3)处理string对象中的字符::: tipC++程序的头文件应该使用cname,而不应该使用name.h的形式:::遍历给定序列中的每个值执行某种操作

for (declaration : expression)
statement

标准库类型 vector

标准库vector表示对象的集合,其中所有对象的类型都相同。vector是一个类模板,而不是类型。(1)定义和初始化vector对象

vector<T> v1;
vector<T> v2(v1);
vector<T> v2 = v1;
vector<T> v3(n, val);
vector<T> v4(n);
vector<T> v5{a,b,c...}
vecrot<T> v5={a,b,c...}

如果用圆括号,那么提供的值是用来构造vector对象的。如果用花括号,则是使用列表初始化该vector对象。(2)向vector对象添加元素先定义一个空的vector对象,在运行的时候使用push_back向其中添加具体指。(3)其他vector操作

v.empty();
v.size();
v.push_back(t);
v[n];

::: warning只能对确认已存在的元素执行下标操作。:::

迭代器介绍

迭代器运算符

*iter            // 解引用,返回引用
iter->mem // 等价于 (*iter).mem
++iter
--iter
iter1 == iter2
iter1 != iter2
iter + n
iter - n
iter += n
iter -= n
iter1 - iter2 // 两个迭代器相减的结果是它们之间的距离
>, >=, <, <= // 位置比较

::: warning凡是使用了迭代器的循环体,都不能向迭代器所属的容器添加元素。:::

数组

(1)数组、指针使用数组下标的时候,通常将其定义为size_t类型。::: warning定义数组必须指定数组的类型,不允许用auto推断。不存在引用的数组。如果两个指针分别指向不相关的对象,则不能进行对这2个指针进行比较。:::

多维数组

多维数组实际上是数组的数组。

size_t cnt = 0;
for(auto &row : a)
for (auto &col : row){
col = cnt;
++cnt;
}
int *ip[4];    // 整型指针的数组
int (*ip)[4]; // 指向含有4个整数的数组

术语

begin string和vector的成员,返回指向第一个元素的迭代器。也是一个标准库函数,输入一个数组,返回指向该数组首元素的指针。end string和vector的成员,返回一个尾后迭代器。也是一个标准库函数,输入一个数组,返回指向该数组尾元素的下一个位置的指针。
第四章 表达式
P120-P151

4.1 基础

重载运算符:为已经存在的运算符赋予了另外一层含义。左值、右值:当一个对象用作右值得时候,用的是对象的值(内容)。当对象被用作左值得时候,用的是对象的身份(在内存中的位置)。

4.2 算术运算符

%:参与取余运算的运算对象必须是整数类型。

4.3 逻辑和关系运算符

&& 运算符和 || 运算符都是先求左侧运算对象的值再求右侧运算对象的值。   
::: warning进行比较运算的时候,除非比较的对象是bool类型,否则不要使用布尔字面值true,false作为运算对象。:::

4.4 赋值运算符

赋值运算符满足右结合律。不要混淆相等运算符和赋值运算符

if (i = j)

if (i == j)

4.5 递增和递减运算符

递增运算符 ++递减运算符 —

4.6 成员访问运算符

点运算符和箭头运算符

n = (*p).size();
n = p->size();

4.7 条件运算符

condition ? expression1 : expression2;

4.8 位运算符

4.9 sizeof运算符

sizeof运算符返回一条表达式或一个类型名字所占的字节数,其所得值是一个size_t类型,是一个常量表达式。

sizeof (type)
sizeof expr

4.10 逗号运算符

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。

4.11 类型转换

隐式转换显式转换命名的强制类型转换

cast-name<type>(expression)

// cast-name是static_cast,dynamic_cast,const_cast,reinterpret_cast

::: tip由于强制类型转换干扰了正常的类型检查,因此建议避免强制类型转换。:::

4.12 运算符优先级表

第五章 语句
P154-P1785.1 简单语句(1)空语句

;    // 空语句

(2)复合语句复合语句是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块(block)。

{}

5.2 语句作用域

定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量就超出其作用范围。

5.3 条件语句

(1)if 语句(2)switch 语句case关键字和它对应的值一起被称为case标签。case标签必须是整形常量表达式。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显示的中断了这一过程。dedault 标签:如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后面的语句。

5.4 迭代语句

(1)while 语句

while (condition)
statement

(2)传统 for 语句

for (initializar; condition; expression)
statement

for 语句中定义的对象只在for循环体内可见。(3)范围 for 语句

for (declaration : expression)
statement

(4)do while 语句

do 
statement
while (condition)

5.5 跳转语句

breakbreak只能出现在迭代语句或者switch语句内部。仅限于终止离它最近的语句,然后从这些语句之后的第一条语句开始执行。continuecontinue语句终止最近的循环中的当前迭代并立即开始下一次迭代。gotogoto的作用是从goto语句无条件跳转到同一函数内的另一条语句。容易造成控制流混乱,应禁止使用。return

5.6 try语句块和异常处理

C++中异常处理包括:throw表达式、try语句块。try和catch,将一段可能抛出异常的语句序列括在花括号里构成try语句块。catch子句负责处理代码抛出的异常。throw表达式语句,终止函数的执行。抛出一个异常,并把控制权转移到能处理该异常的最近的catch字句。
第六章 函数
P182-P225

6.1 函数基础

(1)形参和实参:实参的类型必须与对应的形参类型匹配。函数的调用规定实参数量应与形参数量一致。(2)局部对象形参和参数体内部定义的变量统称为局部变量,它们对函数而言是”局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏外层作用域中同名的其他变量。自动对象:只存在于块执行期间的对象。局部静态对象:在程序的执行路径第一次经过对象定义语句时候进行初始化,并且直到程序终止才会被销毁。

size_t count_calls()
{
static size_t ctr = 0;
return ++ctr;
}

(3)函数声明函数的三要素:(返回类型、函数名、形参类型)。函数可被声明多次,但只能被定义一次。(4)分离式编译分离式编译允许把程序分割到几个文件中去,每个文件独立编译。编译->链接

6.2 参数传递

当形参是引用类型,这时它对应的实参被引用传递或者函数被传引用调用。当实参被拷贝给形参,这样的实参被值传递或者函数被传值调用。(1)传值参数(2)被引用传参(3)const形参和实参(4)数组形参为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

void print(const int*);
void pring(const int[]);
void print(const int[10]);
// 以上三个函数等价

数组引用实参:f(int (&arr)[10])

int *matrix[10];   // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针

(5)含有可变形参的数组initializer_list

for err_msg(initializer_list<string> li)

6.3 返回类型和return语句

2种:无返回值函数和右返回值函数。

return;
return expression;

函数完成后,它所占用的存储空间也会随着被释放掉。::: warning返回局部对象的引用是错误的;返回局部对象的指针也是错误的。:::

6.4 函数重载

重载函数:同一作用域内的几个函数名字相同但形参列表不通,我们称之为重载函数。(overloaded)。不允许2个函数除了返回类型外其他所有的要素都相同。重载与作用域如果在内存作用域中声明名字,它将隐藏外层作用域中声明的同名实体。

6.5 特殊用途语言特性

(1)默认实参函数调用时,实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char background = ' ');

::: tip当设计含有默认实参的函数时,需要合理设置形参的顺序。一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。:::(2)内联函数使用关键词inline来声明内联函数。内联用于优化规模较小,流程直接,频繁调用的函数。(3)constexpr函数constexpr函数是指能用于常量表达式的函数。

6.6 函数匹配

Step1:确定候选函数和可选函数。Step2:寻找最佳匹配。

6.7 函数指针

函数指针指向的是函数而非对象。

void useBigger (const string &s1, const string &s2, bool pf(const string &, const string &));
等价于
void useBigger (const string &s1, const string &s2, bool (*pf)(const string &, const string &));

第七章 类
P228-P273类的基本思想是数据抽象和封装。抽象是一种依赖于接口和实现分离的编程技术。封装实现了类的接口和实现的分离。

7.1 定义抽象数据类型

(1)this任何对类成员的直接访问都被看作this的隐式引用。

std::string isbn() const {return bookNo;}

等价于

std::string isbn() const {return this->bookNo;}

(2)在类的外部定义成员函数类外部定义的成员的名字必须包含它所属的类名。

double Sales_data::avg_price() const {
if (units_sol)
return revenue/units_sols;
else
return 0;
}

(3)构造函数定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数没有返回类型;构造函数的名字和类名相同。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。编译器创建的构造函数被称为合成的默认构造函数。::: tip只有当类没有声明任何构造函数的时,编译器才会自动的生成默认构造函数。一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数:::

7.2 访问控制与封装

(1)访问控制

说明符用途
public使用public定义的成员,在整个程序内可被访问,public成员定义类的接口。
private使用private定义的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

(2)友元类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。以friend关键字标识。友元不是类的成员,不受访问控制级别的约束。::: tip友元的声明仅仅制定了访问的权限,而非通常意义的函数声明。必须在友元之外再专门对函数进行一次声明。:::

// Sales_data.h

class Sales_data {
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
}

// nonmember Sales_data interface functions
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

//Sales_data.cpp

Sales_data
add(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; // copy data members from lhs into sum
sum.combine(rhs); // add data members from rhs into sum
return sum;
}

// transactions contain ISBN, number of copies sold, and sales price
istream&
read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}

ostream&
print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}

7.3 类的其他特性

(1)重载成员变量

Screen myScrren;
char ch = myScreen.get();
ch = myScreen.get(0,0);

(2)类数据成员的初始化类内初始值必须使用=或者{}的初始化形式。

class Window_mgr{
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
}

(3)基于const的重载

class Screen {
public:
// display overloaded on whether the object is const or not
Screen &display(std::ostream &os)
{ do_display(os); return *this; }
const Screen &display(std::ostream &os) const
{ do_display(os); return *this; }
}

当某个对象调用display的时候,该对象是否是const决定了应该调用display的哪个版本。(3)类类型对于一个类来说,在我们创建他的对象之前该类必须被定义过,而不能仅被声明。(4)友元友元类如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

class Screen {
// Window_mgr的成员可以访问Screen类的私有部分
friend class Window_mgr;
}

令成员函数作为友元

class Screen {
// Window_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex);
}

7.4 类的作用域

一个类就是一个作用域。

7.5 构造函数再探

(1)构造函数的初始值有时必不可少::: tip如果成员是const、引用,或者属于某种未提供默认构造函数的类类型化。我们必须通过构造函数初始值列表为这些成员提供初值。:::

class ConstRef{
public:
ConstRef (int i);
private:
int i;
const int ci;
int &ri;
};

ConstRef:ConstRef(int ii) : i(ii), ci(ii), ri(i){ }

(2)成员初始化的顺序成员初始化的顺序与它们在类定义中出现 的顺序一致。P259(3)委托构造函数使用它所述类的其他构造函数执行它自己的初始化过程。(4)如果去抑制构造函数定义的隐式转换?在类内声明构造函数的时候使用explicit关键字。

7.6 类的静态成员

(1)声明静态成员在成员的声明之前加上关键词static。类的静态成员存在于任何对象之外,对象中不包含任何与静态成员有关的数据。(2)使用类的静态成员

double r;
r = Account::rate();

小结

类有两项基本能力:一是数据数据抽象,即定义数据成员和函数成员的能力;二是封装,即保护类的成员不被随意访问的能力。
第八章 IO库
P278-P290C++语言不直接处理输入输出,而是通过一组定义在标准库中的类型来处理IO。

  • iostream处理控制台IO
  • fstream处理命名文件IO
  • stringstream完成内存string的IO

ifstream和istringstream继承自istreamofstream和ostringstream继承自ostream

8.1 IO类

(1)IO对象无拷贝或复制。进行IO操作的函数通常以引用方式传递和返回流。(2)刷新输出缓冲区flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区。

8.2 文件输入输出

作用
ifstream从一个给定文件读取数据
ofstream从一个给定文件写入数据
fstream读写给定文件

8.3 string流

作用
istringstream从string读取数据
ostringstream向string写入数据
stringstream既可从string读数据也可以向string写数据
// will hold a line and word from input, respectively
string line, word;

// will hold all the records from the input
vector<PersonInfo> people;

// read the input a line at a time until end-of-file (or other error)
while (getline(is, line)) {
PersonInfo info; // object to hold this record's data
istringstream record(line); // bind record to the line we just read
record >> info.name; // read the name
while (record >> word) // read the phone numbers
info.phones.push_back(word); // and store them
people.push_back(info); // append this record to people
}
// for each entry in people
for (vector<PersonInfo>::const_iterator entry = people.begin();
entry != people.end(); ++entry) {
ostringstream formatted, badNums; // objects created on each loop

// for each number
for (vector<string>::const_iterator nums = entry->phones.begin();
nums != entry->phones.end(); ++nums) {
if (!valid(*nums)) {
badNums << " " << *nums; // string in badNums
} else
// ``writes'' to formatted's string
formatted << " " << format(*nums);
}
if (badNums.str().empty()) // there were no bad numbers
os << entry->name << " " // print the name
<< formatted.str() << endl; // and reformatted numbers
else // otherwise, print the name and bad numbers
cerr << "input error: " << entry->name
<< " invalid number(s) " << badNums.str() << endl;
}

第九章 顺序容器
P292-P332顺序容器为程序员提供了控制元素存储和访问顺序的能力。

9.1 顺序容器概述

类型作用
vector可变数组大小。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。
deque双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。
list双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。
forward_list单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。
array固定大小数组。支持快速随机访问。不能添加或删除元素。
string与vector相似的容器,但专门用于保存字符、随机访问快。在尾部插入/删除速度快。

9.2 容器库概述

一般,每个容器都定义在一个头文件中。容器均定义为模板类。

类型别名
iterator此容器类型的迭代器类型
const_iterator可以读取元素,但不能修改元素的迭代器类型
size_type无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type带符号整数类型,足够保存两个迭代器之间的距离
value_type元素类型
reference元素的左值诶性:与value_type&含义相同
const_reference元素的const左值类型(即,const value_type&)
构造函数
C c;默认构造函数,构造空容器
C c1(c2)构造c2的拷贝c1
C c(b, e)构造c,将迭代器b和e指定的范围内的元素拷贝到c(array不支持)
C c{a, b, c…}列表初始化c
赋值与swap
c1=c2将c1中的元素替换为c2中元素
c1 = {a, b, c…}将c1中的元素替换为列表中元素(不适用于array)
a.swap(b)交换a和b的元素
swap(a, b)与a.swap(b)等价
大小
c.size()c中元素的数组(不支持forward_list)
c.max_size()c中可保存的最大元素数目
c.empty()若c中存储了元素,返回false,否则返回true
添加/删除元素(不适用于array)
c.insert(args)将args中的元素拷贝进c
c.emplace(inits)使用inits构造c中的一个元素
c.erase(args)删除args指定的元素
c.clear()删除c中的所有元素,返回void
关系运算符
==, !=所有容器都支持相等(不等运算符)
<,<=,>,>=关系运算符(无序关联容器不支持)
获取迭代器
c.begin(), c.end()返回指向c的首元素和尾元素之后位置的迭代器
c.cbengin(),c.cend()返回const_iterator
反向容器的额外成员(不支持forward_list)
reverse_iterator按逆序寻址元素的迭代器
const_reverse_iterator不能修改元素的逆序迭代器
c.rbegin(), c.rend()返回指向c的尾元素和首元素之前位置的迭代器
c.crbegin(), c.crend()返回const_reverse_iterator

(1)迭代器标准库的迭代器允许我们访问容器中的元素,所有迭代器都是通过解引用运算符来实现这个操作。一个迭代器返回由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。它们标记了容器中元素的一个范围。左闭合区间:[begin, end)

while (begin !=end){
*begin = val;
++begin;
}

(2)容器类型成员见概述通过别名,可以在不了解容器中元素类型的情况下使用它。(3)begin和end成员begin是容器中第一个元素的迭代器end是容器尾元素之后位置的迭代器(4)容器定义和初始化P290

C c;            // 默认构造函数
C c1(c2)
C c1=c2
C c{a,b,c...} // 列表初始化
C c={a,b,c...}
C c(b,e) // c初始化为迭代器b和e指定范围中的元素的拷贝
// 只有顺序容器(不包括array)的构造函数才能接受大小参数
C seq(n)
C seq(n,t)

将一个容器初始化为另一个容器的拷贝:当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型相同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。标注库array具有固定大小:不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。P301(5)赋值与swaparrray类型不允许用花括号包围的值列表进行赋值。

array<int, 10> a2={0}; //所有元素均为0
s2={0}; // 错误!

seq.assign(b,e)   // 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。swap用于交换2个相同类型容器的内容。调用swap之后,两个容器中的元素将交换。(6)容器大小操作size 返回容器中元素的数目empty 当size为0返回布尔值true,否则返回falsemax_size 返回一个大于或等于该类型容器所能容纳的最大元素数的值(7)关系运算符关系运算符左右两边的元素符对象必须是相同类型的容器。::: tip只有当元素类型也定义了相应的比较运算符,才可以使用关系元素安抚来比较两个容器:::

9.3 顺序容器操作

(1)向顺序容器添加元素表格P305使用push_back:追加到容器尾部使用push_front:插入到容器头部在容器中的特定位置添加元素:使用insert

vector <string> svec;
svec.insert(svec.begin(), "Hello!");

插入范围内元素:使用insert使用emplace操作:emplace_front、emplace和emplace_back分别对应push_front、insert和push_back。emplace函数直接在容器中构造函数,不是拷贝。(2)访问元素P309注意end是指向的是容器尾元素之后的元素。

在顺序容器中访问元素的操作
c.back()返回c中尾元素的引用。若c为空,函数行为未定义
c.front()返回c中首元素的引用。若c为空,哈数行为未定义
c[n]返回c中下标为n的元素的引用,n是一个无符号整数。若n>=size(),则函数行为未定义
c.at[n]返回下标为n的元素的引用。如果下标越界,则抛出out_of_range异常

(3)删除元素

顺序容器的删除操作
c.pop_back()删除c中尾元素。若c为空,则函数行为未定义。返回返回void
c.pop_front()删除c中首元素。若c为空,则函数行为未定义。返回void
c.erase(p)删除迭代器p所指定的元素,返回一个指向被删除元素之后元素的迭代器,如p指向尾元素,则返回尾后(off-the-end)迭代器。若p是尾后迭代器,则函数行为未定义
c.erase(b, e)删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删除元素之后元素的迭代器。若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.claer()删除c中的所有元素。返回void

(4)特殊的forwar_list操作P313befor_begin();cbefore_begin();insert_after;emplace_after;erase_after;(5)改变容器大小reseize用于扩大或者缩小容器。resize操作接受一个可选的元素值参数,用来初始化添加到容器内的元素。如果容器保存的是类类型元素,且resize向容器中添加新元素,则必须提供初始值,或者元素类型必须提供一个默认构造函数。

9.4 vector对象是如何增长的

为了避免影性能,标准库采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector和string通常会分配比新的新的空间需求更大的内存空间。容器预留这些空间作为备用,可以用来保存更多的新元素。容器管理的成员函数:

容器大小管理操作
c.shrink_to_fit()请将capacity()减少为与size()相同大小
c.capacity()不重新分配内存空间的话,c可以保存多少元素
c.reverse()分配至少能容纳n个元素的内存空间。reverse并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。调用reverse永远不减少容器占用的内存空间。

capcacity和size:区别:容器的size是指它已经保存的元素的数目;capcacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。注意:只有当迫不得已时才可以分配新的内存空间。

9.5 额外的string操作

(1)构造string的其他方法

构造string的其他方法
string s(cp, n)s是cp指向的数组中前n个字符的拷贝
string s(s2, pos2)s是string s2从下标pos2开始的字符的拷贝。
string s (s2, pos2, len2)s是string s2从下标pos2开始len2个字符的拷贝

substr操作:substr操作返回一个string,它是原始string的一部分或全部的拷贝。s.substr(pos, n)  返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size() – pos, 即拷贝从pos开始的所有字符(2)改变string的其他方法assign  替换赋值,总是替换string中的所有内容insert  插入append 末尾插入,总是将新字符追加到string末尾replace 删除再插入(3)string搜索操作

string搜索操作
s.find(args)查找s中args第一次出现的位置
s.rfind(args)查找s中args最后一次出现的位置
s.find_first_of(args)在s中查找args中任何一个字符第一次出现的位置
s.find_last_of(args)在s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args)在s中查找第一个不在args中的字符
s.find_last_not_of(args)在s中查找最后一个不在args中的字符

(4)compare函数compare有6个版本,P327(5)数值转换P328tostringstod

9.6 容器适配器

顺序容器适配器:stack; queue; priority_queue;适配器是一种机制,能使某种事物看起来像另外一种事物。定义一个适配器:适配器有2个构造函数:1、默认构造函数创建一个空对象2、接受一个容器的构造函数栈适配器:

栈的操作
s.pop()删除栈顶元素,但不返回该元素值
s.push(item)创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造
s.emplace(args)由arg构造
s.top()返回栈顶元素,但不将元素弹出栈

队列适配器:

queue和priority_queue操作
q.pop()返回queue的首元素或priority_queue的最高优先级的元素,但不删除此元素
q.front()                  q.back()返回首元素或尾元素,但不删除此元素。只适用于queue
q.top()返回最高优先级元素,但不删除该元素。只适用于priority_queue
q.push(item) q.empalce(args)在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item,或者由args构造

术语

begin容器操作:返回一个指向容器首元素的迭代器,如果容器为空,则返回尾后迭代器。是否返回const迭代器依赖于容器的类型。cbegin容器操作:返回一个指向容器尾元素之后的const_iterator。
第十章 泛型算法
P336-P371标准库并未给每个容器添加大量功能,而是提供了一组算法。这些算法是通用的,可以用于不同类型的容器和不同类型的元素。

10.1 概述

头文件:algorithm、numeric算法不依赖于容器,但算法依赖于元素类型的操作。

10.2 初识泛型算法

(1)只读算法accumulate  求和equal 是否相等(2)写容器元素的算法算法不检查写操作拷贝算法:copy重排容器元素的算法:sort::: tip标准库函数对迭代器而不是容器进行操作。因此,算法不能直接添加或删除元素:::

10.3 定制操作

标准库允许我们提供自己定义的操作来代替默认运算符。(1)向算法传递函数谓词:谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法的谓词分为两类:1、一元谓词:只接受单一参数。2、二元谓词:接受两个参数。

bool isShorter(const string &s1, const string &s2)
{
retrun s1.size() < s2.size();
}

sort(words.begin(), words.end(), isShorter);

排序算法:stable_sort算法维持相等元素的原有顺序。(2)lambda表达式lamba:lambda表达式表示一个可调用的代码单元。一个lambda具有一个返回类型、一个参数列表和一个函数体。

[capture list](parameter list) -> return type {function body}
// capture list 捕获列表,lambda所在函数中定义的局部变量
// 捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字
// lambda必须使用尾置返回来指定返回类型

(3)lambda捕获和返回两种:值捕获、引用捕获::: warnning当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。一般的,应该尽量减少捕获的数据量,来避免潜在的问题。如果可能,避免捕获指针或引用。:::隐式捕获:当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。显式捕获的变量必须使用与隐式捕获不同的方式。lambda捕获列表 P352可变lambda:若希望改变一个被捕获的变量的值,必须在参数列表首加上关键字mutable。指定lambda返回类型:当需要为lambda定义返回类型时,必须使用尾置返回类型。(4)参数绑定标准库bind函数:

auto newCallable = bind(callable, arg_list);
// 调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数

10.4 再探迭代器

插入迭代器、流迭代器、反向迭代器、移动迭代器(1)插入迭代器back_inserter:创建一个使用push_back的迭代器front_inserter:创建一个使用push_front的迭代器inserter:创建一个使用inserter的迭代器(2)iostream迭代器istream_iterator 读取输入流ostream_iterator 向一个输出流写数据istream_iterator操作:

istream-iterator操作
istream_iterator<T> in(is);in从输入流is读取类型为T的值
istream_iterator<T> end;读取类型为T的值得istream_iterator迭代器,表示尾后位置
in1 == in2              in1 != in2in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等
*in返回从流中读取的值
in->mem与(*in).mem含义相同
++in, in++用>>从输入流读取下一个值

ostream_iterator操作:

ostream_iterator操作
ostream_iterator<T> out(os);out将类型为T的值写到输出流os中
ostream_iterator<T> out(os, d);out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符串结尾的字符数组
out = val用<<将val写入到out所绑定的ostream中
*out, ++out, out++

(3)反向迭代器反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。

10.5 泛型算法结构

迭代器类别
输入迭代器只读、不写;单遍扫描,只能递增
输出迭代器只写,不读;单遍扫描,只能递增
前向迭代器可读写;多遍扫描,只能递增
双向迭代器可读写,多遍扫描,可递增递减
随机访问迭代器可读写,多遍扫描,支持全部迭代器运算

10.6 特定容器算法

对于list、forward_list,应该优先使用成员函数的算法而不是通用算法。

术语

cref标准库函数:返回一个可拷贝的对象,其中保存了一个指向不可拷贝类型的const对象的引用第十一章 关联容器
P374-P397关联容器支持高效的关键字查找和访问。

类型备注
map关联数组,保存关键字-值对
set值保存关键字的容器
multimap关键字可重复出现的map
multiset关键字可重复出现的set
unordered_map用哈希函数组织的map
unordered_set用哈希函数组织的set
unordered_multimap哈希组织的map;关键字可以重复出现
unordered_multiset哈希组织的set;关键字可以重复出现

11.1 使用关联容器

map是关键词-值对的集合。为了定义一个map,我们必须指定关键字和值的类型。

// 统计每个单词在输入中出现的次数
map<string, size_t> word_count;
string word;
while (cin >> word)
++word_count[word];
for (const auto &w : word_count)
count << w.first << " cccurs " < w.second
<< ((w.second > 1) ? " times" : "time") << endl;

set是关键字的简单集合。为了定义一个set,必须指定其元素类型。

// 统计输入中每个单词出现的次数,并忽略常见单词
map<string, size_t> word_count;
set<string> exclude = {"the", "But"};
string word;
while (cin >> word)
// 只统计不在exclude中的单词
if (exclude.find(word) == exclude.end())
++word_count[word]; //获取并递增word的计数器

11.2 关联容器概述

(1)定义关联容器定义map时,必须指明关键字类型又指明值类型;定义set时,只需指明关键字类型。(2)pair类型pair标准库类型定义在头文件utility中。一个pair保存两个数据成员。当创建一个pair时,必须提供两个类型名。

pair<string, string> anon; // 保存两个string
pair<string, string> author{"James", "Joyce"}; // 也可为每个成员提供初始化器

pair的数据类型是public的,两个成员分别命名为first和second。pair上的操作,见表,P380

11.3 关联容器操作

关联容器额外的类型别名
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型,只适用于map
value_type对于set,与key_type相同;对于map,为pair<const key_type, mapped_type>

(1)关联容器迭代器set的迭代器是const的set和map的关键字都是const的遍历关联容器:map和set都支持begin和end操作。使用beigin、end获取迭代器,然后用迭代器来遍历容器。(2)添加元素

关联容器insert操作
c.insert(v)v是value_type类型的对象;
c.emplace(args)args用来构造一个元素
c.insert(b, e)
c.insert(il)
c.insert(p, v)
c.emplace(p ,args)

(3)删除元素

从关联容器删除元素
c.erase(k)从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量
c.erase(p)从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回.end()
c.erase(b, e)删除迭代器b和e所表示的范围中的元素。返回e

(4)map的下标操作

map和unorder_map的下标操作
c[k]返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at[k]访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常

::: tipmap进行下标操作,会获得mapped_type对象;当解引用时,会得到value_type对象。:::(5)访问元素

c.find(k)  // 返回一个迭代器,指向第一个关键字k的元素,如k不在容器中,则返回尾后迭代器
c.count(k) // 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1
c.lower_bound(k) // 返回一个迭代器,指向第一个关键字不小于k的元素;不适用于无序容器
c.upper_bound(k) // 返回一个迭代器,指向第一个关键字大于k的元素;不适用于无序容器
c.equal_bound(k) // 返回一个迭代器pair,表示关键字等于k的元素的范围。如k不存在,pair的两个成员均等于c.end()

11.4 无序容器

无序容器使用关键字类型的==运算符和一个hash<key_type>类型的对象来组织元素。无序容器在存储上组织为一组桶,适用一个哈希函数将元素映射到桶。无序容器管理操作,表格,P395还可以自定义自己的hash模板 P396

using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
SD_multiset bookStore(42, haser, eqOp);

第十二章 动态内存
P400-P436

12.1 动态指针与智能指针

智能指针用途
shared_ptr提供所有权共享的智能指针:对共享对象来说,当最后一个指向它的shared_ptr被销毁时会被释放。
unique_ptr提供独享所有权的智能指针:当unique_ptr被销毁的时,它指向的独享被释放。unique_ptr不能直接拷贝或赋值。
weak_ptr一种智能指针,指向由shared_ptr管理的对象。在确定是否应释放对象视,shared_ptr并不把weak_ptr统计在内。

(1)shared_ptr类

shared_ptr<string> p1;

make_shared函数:make_shared在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。

share_ptr<int> p3 = make_shared<int>(42);

shared_ptr的拷贝和赋值:每个shared_ptr都有一个关联的计数器,称为引用计数。一旦一个shared_ptr的引用计数变为0,就会自动释放自己所管理的对象。(2)直接管理内存运算符new分配分配内存,delete释放new分配的内存。使用new动态分配和初始化对象:

// 默认情况下,动态分配的对象是默认初始化的
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象
// 直接初始化方式
int *pi = new int(1024); // pi指向的对象的值为1024
// 对动态分配的对象进行值初始化,只需在类型名之后加上一对空括号
int *pi1 = new int; // 默认值初始化;*pi1的值未定义
int *pi2 = new int(); // 值初始化为0;*pi2为0

动态分配的const对象:

const int *pci = new const int(1024);

释放动态内存:

delete p;

delete表达式执行两个动作:销毁给定的指针指向的对象;释放对应的内存。(3)unique_ptr某个时刻,只能有一个unique_ptr指向一个给定对象。当unique_ptr销毁时,它所指向的对象也被销毁。

unique_ptr操作
unique_ptr<T> u1
unique_ptr<T, D> u2
unique_ptr<T, D> u(d)
u = nullptr
u.release()
u.reset()
u.reset(p)
u.reset(nullptr)

(4)weak_ptrweak+ptr是一种不受控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,而且不会改变shared_ptr的引用计数。

weak_ptr 操作
weak_ptr<T> w
weak_ptr<T> w(sp)
w = p
w.reset()将w置空
w.use_count()与w共享对象的shared_ptr的数量
w.expired()
w.lock()

使用weak_ptr之前,需要调用lock,检查weak_ptr指向的对象是否存在。

12.2 动态数组

(1)new和数组在类型名之后跟一对方括号,在其中指明要分配的对象的数目。释放动态数组:

delete p;      // p必须指向一个动态分配的对象或为空
delete [] pa; // pa必须指向一个动态分配的数组或为空

智能指针和动态数组

unique_ptr<T []> u;
unique_ptr<T []> u(p);
u[i];

(2)allocator类标准库allocator类定义在头文件memory中,帮助将内存和对象构造分离开来。

allocator<string> alloc;
auto const p = alloc.allocate(n);
表达式作用
allocator[T] a定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为T的对象
a.construct(p, args)为了使用allocate返回的内存,我们必须使用construct构造对象。使用未构造的内存,其行为是未定义的。
a.destroy(p)p为T*类型的指针,此算法对p指向的对象执行析构函数

术语

new : 从自由空间分配内存。new T 分配并构造一个类型为T的指针。如果T是一个数组类型,new 返回一个指向数组首元素的指针。类似的,new  [n]  T 分配 n 个类型为T的对象,并返回指向数组首元素的指针。空悬指针:一个指针,指向曾经保存一个对象但现在已释放的内存。智能指针:标准库类型。负责在恰当的时候释放内存。
第十三章 拷贝控制
P440-P486五种拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。拷贝构造函数、移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝赋值运算符、移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。

13.1 拷贝、赋值与销毁

(1)拷贝构造函数拷贝构造函数的第一个参数必须是一个引用类型。

class Foo {
public :
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
}

合成拷贝构造函数:若未定义拷贝构造函数,编译器会定义一个。拷贝初始化:拷贝初始化,要求编译器将右运算对象拷贝到正在创建的对象中。拷贝初始化通常使用拷贝构造函数来完成。(2)拷贝赋值运算符重载赋值运算符:oprator=合成拷贝赋值运算符:若一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。(3)析构函数析构函数:用于释放对象使用的资源,销毁对象的非static数据成员。

class Foo {
public:
~Foo(); // 析构函数,一个类只会有唯一一个析构函数。
}

在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。销毁类类型的成员需要执行成员自己的析构函数。合成析构函数:当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。析构函数体本身并不直接销毁成员。(4)三五法则P447需要析构函数的类也需要拷贝和赋值操作需要拷贝操作的类也需要赋值操作,反之亦然(5)使用default=将拷贝控制成员定义为=dafault来显式地要求编译器生活才能合成的版本。

class Sales_data {
public:
Sales_data(const Sales_data&) = default;
}

(6)阻止拷贝在函数参数列表后面加上=delete。=delete必须出现在函数第一次声明的时候。析构函数不能是删除的成员合成的拷贝控制成员可能是删除的:如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

13.2 拷贝控制和资源管理

(1)行为像值的类为了提供类值的行为,对于类管理的对象,每个对象都应该拥有一份自己的拷贝。类值拷贝赋值运算符:通常组合了析构函数和构造函数的操作。

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}

(2)行为像指针的类如果需要可直接管理资源,可以使用引用计数。

13.3 交换操作

swap

13.4 拷贝控制示例

P460

13.5 动态内存管理类

P464

13.6 对象移动

与任何赋值运算符一样,移动赋值运算符必须销毁左侧运算对象的旧状态。(1)右值引用可通过move函数开获得绑定到左值上的右值引用。

int && rr3 = std::move(rr1);

(2)移动构造函数和移动赋值运算符移动构造函数的第一个参数是该类类型的一个右值引用。移动赋值运算符:

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{

}

合成的移动操作:若一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。如果一个类没有移动操作,类会使用对应的拷贝操作来代替移动操作。移动迭代器:移动迭代器的解引用运算符生成一个右值引用。(3)右值引用和成员函数::: tip区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&。:::右值和左值引用成员函数:指出this的左值/右值属性的方式与定义const成员函数相同,在参数列表后放置一个引用限定符。P483::: tip如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。P485:::

术语

引用限定符:被&限定的函数只能用于坐值;被&&限定的函数只能用于右值。
第十四章 重载运算与类型转换
P490-P523通过运算符重载可重新定义该运算符的含义。

14.1 基本概念

定义:重载运算符是具有特殊名字的函数。名字由operator和符号组成。重载运算符包含返回类型、参数列表和函数体。::: tip当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。对于一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数。我们只能重载已有的运算符。:::直接调用一个重载的运算符函数

data1 + data2;
operator+(data1, data2);
// 以上2个调用等价

14.2 输入和输出运算符  

(1)重载输出运算符<<

ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.unites_sold << " " << item.revenue << " " << item.avg_price();
return os;
}

(2)重载输入运算符>>

istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> item.units_sold >> price;
if (is)
item.revenue = items.units_sold * price;
else
item = Sales_data();
return is;
}

14.3 算术和关系运算符

(1)相等运算符

bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.unites_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}

(2)关系运算符operator<

14.4 赋值运算符

operator=operator+=

14.5 下标运算符

operator[]下标运算符必须是成员函数。

class StrVec{
public:
std::string& operator[](std::size_t n){
return elements[n];
}
const std::string& operator[](std::size_t n) const{
return elements[n];
}
private:
std::string *elements;
}

14.6 递减和递增运算符

递增运算符(++)递减运算符(–)定义前置递增/递减运算符:

class StrBlobPtr{
public:
StrBlobPtr& operator++(); // 前置运算符
StrBlobPtr& operator--();
}

区分前置和后置运算符:

class StrBlobPtr{
public:
StrBlobPtr operator++(int); // 后置运算符
StrBlobPtr operator--(int);
}

14.7 成员访问运算符

operator*operator->

14.8 函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。

struct absInt{
int operator()(int val) const {
return val < 0 ? -val : val;
}
};

absInt absObj;
int ui = absObj(i);

如果定义了调用运算符,则该类的对象称为函数对象。

14.9 重载、类型转换与运算符

(1)类型转换运算符类型转换运算符是类的一种特殊成员函数,将一个类类型的值转换成其他类型。形式:operator type() const;(2)避免有二义性的类型转换(3)函数匹配与重载运算符::: warning如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,将会遇到重载运算符与内置运算符的二义性问题。:::

术语

类类型转换:由构造函数定义的从其他类型到类类型的转换以及由类型转换运算符定义的从类类型到其他类型的转换。
第十五章 面向对象程序设计
P526-P575

15.1 OOP:概述

(1)面对对象程序设计(object-oriented programming)的核心思想:数据抽象、继承和动态绑定。(2)继承:继承是一种类联系在一起的一种层次关系。这种关系中,根部是基类,从基类继承而来的类成为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。虚函数:virtual function。基类希望派生类各自定义自身版本的函数。

class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
}

(3)动态绑定:::: tip在C++语言中,当我们使用基类的引用(或者指针)调用一个虚函数时将发生动态绑定(也称运行时绑定)。P527:::

15.2 定义基类和派生类

(1)定义基类虚函数:基类希望派生类进行覆盖的函数。基类将该函数定义为虚函数(virtual)。基类通过在其成员函数的声明语句之前加上关键词virtual使得该函数执行动态绑定。关键词virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式的也是虚函数。(2)定义派生类派生类必须通过派生类列表明确指出它是从哪个基类继承而来的。

class Bulk_quote : public Quote {
... // 省略
}

对于派生类中的虚函数的处理:若派生类未覆盖基类中的虚函数,则该虚函数的行为类似其他普通成员。C++允许派生类显式注明覆盖了基类的虚函数,可通过添加override关键字。派生类对象:一个派生类对象包含多个部分:自己定义的成员的子对象,以及基类的子对象。派生到基类的类型转换:由于派生类对象中含有与其基类对象的组成部分,因此可以进行隐式的执行派生类到基类的转换。

Quote item;        // 基类
Bulk_quote bulk; // 派生类
Quote *p = &item; // p指向Quote对象
p = &bulk; // p指向bulk的Quote部分
Quote &r = bulk; // r绑定到bulk的Quote部分。

派生类构造函数:每个类控制自己的成员的初始化过程。派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。派生类使用基类的成员:派生类可以访问基类的公有成员和受保护成员。::: tip派生类对象不能直接初始化基类的成员。派生类应该遵循基类的借口,通过调用基类的构造函数来初始化从基类继承来的成员。:::被用作基类的类:若使用某个类作为基类,则该类必须已被定义而非仅仅声明。派生类包含它的直接基类的子对象以及每个间接基类的子对象。防止继承发生:在类名后面跟着一个关键字final。

class NoDerived final {};   // NoDerived不能作为基类

(3)类型转换与继承我们可以将基类的指针或引用绑定到派生类对象上。静态类型与动态类型:静态类型:在编译时已知,是变量声明时的类型或表达式生成的类型。动态类型:运行时才可知,是变量或表达式表示的内存中的对象的类型。如果表达式既不是引用也不是指针,则动态类型与静态类型永远一致。不存在基类向派生类隐式类型转换:

Quote base;
Bulk_quote *bulkP = &base; // 错误!
Bulk_quote *bulkRef = base; // 错误!

::: warning当我么用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。:::

15.3 虚函数

C++的多态性:使用这些类型的多种形式,而无须在意它们的差异。派生类中的虚函数:一个派生类如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。final和override说明符:如果用override标记了某个函数,但是该函数并没有覆盖已存在的虚函数,此时编译器将报错。如果用final标记了某个函数, 则之后任何尝试覆盖该函数的操作都将错误。虚函数与默认实参:如果虚函数某次被调用使用了默认实参,则该实参值由本次调用的静态类型决定。

15.4 抽象基类

纯虚函数:书写=0可以将一个虚函数说明为纯虚函数(pure virtual),纯虚函数无须定义。不能在类的内部为一个=0的函数提供函数体。

class Disc_quote : public Quote {
public:
double net_price(std::size_t) const = 0;
}

抽象基类:含有纯虚函数的类是抽象基类。不能创建抽象基类的对象。

15.5 访问控制与继承

受保护的成员:派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。P543公有、私有和受保护继承:派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员无影响;对基类成员的访问权限只与基类中的访问说明符有关。派生访问说明符的目的是控制派生类用户对于基类成员的访问权限。改变个别成员的可访问性:通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来。

class Derived : private Base {
public:
using Base::size;
}

::: tip派生类只能为它可访问的名字提供using声明。:::默认的继承保护级别:使用class关键字定义的派生类是私有继承的;使用struct关键字定义的派生类是共有继承的。

class Base {};
struct D1 : Base {}; // 默认public继承
class D2 : Base {}; // 默认private继承

15.6 继承中的类作用域

在编译时进行名字查找:一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。名字冲突与继承:派生类的成员将隐藏同名的基类成员。::: tip出了覆盖继承而来的虚函数外,派生类最好不雅重用其他定义在基类中的名字。:::如果派生类的成员函数与基类的某个成员函数同名,则派生类将在其作用域内隐藏掉该基类成员函数。::: tip非虚函数不会发生动态绑定。:::

15.7 构造函数与拷贝控制

(1)虚析构函数在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。

Quote *itemP = new Quote;
delete itemP; // 调用Quote的析构函数
itemP = new Bulk_quote;
delete itemP; // 调用Bulk_quote的析构函数

虚析构函数会阻止合成移动操作。(2)合成拷贝控制与继承基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实要执行移动操作的时候就要首先在基类中进行显式定义。P554(3)派生类的拷贝控制成员派生类的拷贝或移动构造函数:::: tip默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式的使用基类的拷贝(或移动)构造函数。:::派生类的赋值运算符:派生类的赋值运算符必须显式的为其基类部分赋值。派生类的析构函数:派生类函数只负责销毁由派生类自己分配的资源。

15.8 容器与继承

当使用容器存放继承体系中的对象时,必须采用间接存储的方式。因为不允许在容器中保存不同类型的元素。

术语

覆盖:override,派生类中定义的虚函数如果与基类中定义的同名虚函数与相同的形参列表,则派生类版本将覆盖基类的版本。多态:程序能够通引用或指针的动态类型获取类型特定行为的能力。
第十六章 模板与泛型编程P578-P630(1)控制实例化当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。(2)模板是标准库的基础。生成特定类或者函数的过程称为实例化。(3)术语类模板:模板定义,可从它实例化出特定的类。类模板的定义以关键词template开始,后面跟尖括号对<和>,其内为一个用逗号分隔的一个或多个模板参数的列表,随后是类的定义。函数模板:模板定义,可从它实例化出特定函数。函数模板的定义以关键词template开始,后跟尖括号<和>,其内以一个用逗号分隔的一个或多个模板参数的列表,随后是函数的定义。

转自:https://mp.weixin.qq.com/s/VQURTxhoDzWgPbclBJttNw

推荐给初学者:如何学好C语言?

学习C语言不是一朝一夕的事情,但也不需要花费十年时间才能精通。如何以最小的代价学习并精通C语言是本文的主题。请注意,即使是“最小的代价”,也绝不是什么捷径,而是以最短的时间取得最多的收获,同时也意味着你需要经历艰苦的过程。

 

一、要读就读好书,否则不如不读

 

所有初学者面临的第一个问题便是:如何选择教材。好的开始是成功的一半,选择一本优秀的教材是事半功倍的关键因素。不幸的是,学校通常会帮你指定一本很差劲的C语言课本;而幸运的是,你还可以再次选择。

 

大名鼎鼎的谭浩强教授出了一本《C语言程序设计》,据说发行量有超过400万,据我所知,很多学校都会推荐这本书作为C语言课本。虽然本人的名字(谭浩宇)跟教授仅仅一字之差,但我是无比坚定地黑他这本书的。

 

这本书不是写给计算机专业的学生的,而是给那些需要考计算机等级考试的其它专业学生看的。这本书的主要缺点是:例子程序非常不专业,不能教给你程序设计应该掌握的思考方式;程序风格相当地不好,会让你养成乱写代码的恶习;错误太多,曾经有人指出过这本书的上百个错误,其中不乏关键的概念性错误。好了,这本书我也不想说太多了,有兴趣大家可以百度一下:)

 

Kernighan和Ritchie的《The C Programming Language》(中译名《C程序设计语言》)堪称经典中的经典,不过旧版的很多内容都已过时,和现在的标准C语言相去甚远,大家一定要看最新的版本,否则不如不看。另外,即使是最经典最权威的书,也没有办法面面俱到,所以手边常备一本《C语言参考手册》是十分必要的。

 

《C语言参考手册》就是《C Reference Manual》,是C语言标准的详细描述,包括绝大多数C标准库函数的细节,算得上是最好的标准C语言的工具书。顺便提一句,最新的《C程序设计语言》是根据C89标准修订的,而《C语言参考手册》描述的是C99标准,二者可能会有些出入,建议按照C99标准学习。还有一本《C和指针》,写得也是相当地不错,英文名是《Pointers on C》,特别地强调指针的重要性,算是本书的一个特点吧。

 

不过这本书并不十分适合初学者,如果你曾经学过C语言,有那么一些C语言的基础但又不是很扎实,那么你可以尝试一下这本书。我相信,只要你理解了指针,C语言便不再神秘。

 

如果你已经啃完了一本C语言教材,想要更进一步,那么有两本书你一定要看。首先是《C Traps and Pitfalls》(中译名《C陷井与缺陷》),很薄的一本小册子,内容非常非常地有趣。要注意一点,这本书是二十多年前写成的,里面提到的很多C语言的缺陷都已被改进,不过能够了解一些历史也不是什么坏事。然后你可以挑战一下《Expert C Programming》(中译名《C专家编程》),书如其名,这本书颇具难度,一旦你仔细读完并能透彻理解,你便可以放心大胆地在简历上写“精通C语言”了。

 

切记一个原则,不要读自己目前还看不懂的书,那是浪费生命。如果你看不懂,那你一定是缺失了某些必需基础知识。此时,你要仔细分析自己需要补充哪些内容,然后再去书店寻找讲述的这些内容的书籍。把基础知识补充完毕再回头来学习,才会真正的事半功倍。

 

二、Unix/Linux还是Windows,这是个很大的问题

 

不同的编程环境会造就出不同思维的程序员。Windows的程序员大多依赖集成开发环境,比如Visual Studio,而Unix程序员更加钟爱Makefile与控制台。显而易见,集成开发环境更容易上手,在Windows上学习C语言,只需要会按几个基本的Visutal C++工具栏按钮就可以开始写Hello, World!了,而在Unix下,你需要一些控制台操作的基本知识。

 

有人也许认为Unix的环境更简洁,但习惯的力量是很大的,大家都很熟悉Windows的基本操作,而为了学习C语言去专门装一个Unix系统,似乎有点不划算。

 

对于一个只懂得Windows基本操作、连DOS是什么都不知道的新手而言,尽快做一些有趣而有意义的事情才是最重要的。用C语言写一个小程序远比学习ls、cat等命令有趣,况且我们要专注于C语言本身,就不得不暂时忽略一些东西,比如编译链接的过程、Makefile的写法等等等等。

 

所以我建议初学者应该以Visual C++ 6.0(不是VisualC++ .NET)或者Dev C++作为主要的学习环境,而且千万不要在IDE的使用技巧上过多纠缠,因为今后你一定要转向Unix环境的。Visual C++ 6.0使用很方便,调试也很直观,但其默认的编译器对C标准的支持并不好,而Dev C++使用gcc编译器,对C99的标准都支持良好。

 

使用顺带提一下,很多大学的C语言课程还在使用Turbo C 2.0作为实验环境,这是相当不可取的,原因其一是TC 2.0对C标准几乎没有支持,其二是TC 2.0编译得到的程序是16位的,这对今后理解32位的程序会造成极大的困扰(当然,用djgpp之类的东西可以使TC 2.0编译出32位程序,不过那过于复杂了)。

 

等你学完一本C语言的教材,你一定要转向Unix平台继续学习,几乎所有的C语言高级教程都是基于Unix平台的(比如《C专家编程》)。转变的过程是痛苦的,你需要面对的是各种纷繁复杂的命令,完全不同于Windows平台的思考方式,但是这种痛苦是值得的。

 

Unix与C是共生的,Unix的思考方式和习惯更加符合C语言的思考方式和习惯。在Unix下,你可以找到无数优秀的源代码供你尽情阅读,你可以方便地查看某个库函数的联机手册,还可以看到最优秀的代码风格(说到代码风格,我会专门写一篇文章详细叙述)。

 

归结起来就是一句话:初学C语言,建议使用Windows系统和集成开发环境,在准备向“高手”方向努力时,请先转向Unix平台。

 

三、万事俱备,你就是东风

 

书已选定,环境配置完成,正所谓万事俱备,只欠你自己的努力了。请从书的前言开始,仔细地阅读手头的教材,很多人看书喜欢直接从第一章开始看,这是错误的做法。前言是作者对整本书的大体介绍,作者一般会告诉你需要什么基础才能够顺利阅读本书,这可以帮助你检验自己的基础知识是否已经具备。看完前言,还要浏览一下目录,了解一下书的整体结构,顺便给自己安排一下学习计划。

 

学习C语言,必需注意每一个细节,书上的例子代码一定要自己亲自敲一遍,编译执行输出都跟书上说的一致才能算是学完了一个例子,如果不一致,就要仔细找原因。出了书本上有的例子,自己还要“创造”一些例子,比如学习运算符优先级的时候,可以写几个相同的表达式,在不同的位置加上括号,看看有哪些不同的行为,比如*p++和(*p)++,又比如a = b == c、(a = b) == c和a = (b == c)等等。自己抄的书上的例子以及改造之后的例子,还有自己“创造”的例子,都应该仔细地归类保存,并且要在源代码中写上简短的注释,阐述这个例子的意图。

 

例子之后就是习题了,我建议初学者把所有的习题都独立做一遍,然后对照答案的代码,看看自己的代码有那些不足,再试着修改自己的代码。很多人不重视习题,这是极大的错误,因为作者通常会在习题中说明一些重要的道理,而不是单纯地检验前面的知识。

 

也许你认为这样学习太慢,其实不然。学得细致就不用走回头路,等你学到后面才发现自己前面没搞清楚,那才是真的得不偿失。一般说来,整本书读完,你应该完成数千行乃至上万行的代码,无论是原封不动照抄书上的,还是自己心血来潮写就的,都是今后继续学习的一笔财富。

 

以我自己举例,阅读《Windows核心编程》时(我只阅读了3/4的内容),除了抄书上的代码,还自己写了很多例子,一共有5574行(用unix下的wc工具统计),时隔多日,我早已记不清Windows的系统编程了,但只要花几分钟翻出以前的代码看看,便会重新了然于胸。所谓好记性不如烂笔头,就是这个道理。

 

仔细读书、认真抄写源代码、独立完成习题外加更进一步的实验,最后将所有的代码留下,成为自己的经验和财富,绝对的辛苦,也绝对的事半功倍。当然,这种方式只适合学习需要精通的技术,如果不是学习C语言,你还要具体情况具体分析。

 

写到最后,还有非常非常重要的一点没有提及──代码风格,从最开始学习就必须强迫自己模仿最优秀的代码风格。

-END-
整理文章为传播相关技术,版权归原作者所有 |

转自:https://mp.weixin.qq.com/s/1iqrNCr7MNon70lr1RgqvQ