本文主要是介绍重学c++ Primer Plus纪要(第1-15章),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1. 面向过程与面向对象
面向过程的理念:是强调算法的过程性编程,就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象的理念:强调数据的OOP,设计与问题的本质特性相对应的数据格式,是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
2. c++与c的区别(特性)
(1)面向对象:强调数据,通过类和类的对象来实现,具有3大优势。
a.类的继承
b.多态:C++有两种多态,称为动多态(运行期多态)和静多态(编译器多态),静多态主要是通过模板来实现,而动多态是通过虚函数来实现的。即在基类中存在虚函数(一般为纯虚函数)子类通过重载这些接口,使用基类的指针或者引用指向子类的对象,就可以调用子类对应的函数,动多态的函数调用机制是执行器期才能确定的。
c.封装:隐藏类的属性和实现细节,仅仅对外提供接口,私有成员是在封装体内被隐藏的部分,只有类体内说明的函数(类的成员函数)才可以访问私有成员,而在类体外的函数时不能访问的,公有成员是封装体与外界的一个接口,类体外的函数可以访问公有成员,保护成员是只有该类的成员函数和该类的派生类才可以访问的。
(2)泛型编程:强调独立于特定数据类型,只编写一个泛型函数并将其用于各种实际的数据类型,目的是重用代码,主要实现方式是函数模板、类模板。
3. 一个c++程序运行的完整过程
(1)编辑器写程序,形成源代码;
(2)编译器编译源代码,将源代码翻译成机器语言,形成目标代码;
(3)链接程序将目标代码和其他代码(第三方库和启动代码)链接起来,形成可执行代码。
4. c++程序中main函数返回值返回给谁
将计算机操作系统看做调用主函数的程序,因此main函数返回值返回给了操作系统。
5. cout输出的有点(与c printf的区别)
(1)自动识别数据类型;
(2)可进行运算符重载扩展cout功能。
6. 位(bit)与字节(byte)
位:计算机内存的基本单元,可以看做电子开关,有开(1)和关(1)2种状态。因此8位单元可以表示2的8次方=256个数值范围。
字节:指8位的内存单元,用来度量计算机内存量的单位。
7. 无符号与有符号数据类型及越过边界情况(以整型为例)
(1)默认为有符号类型;
(2)超出最大值,减去最大值后由最小值开始起算(上下限首尾相连)。低于最小值,有最大值开始倒算(上下限首尾相连)。
8. 字符的表示
(1) 使用字符的数值编码来表示,最常用ASCII字符集。
(2)char在默认情况下既不是有符号、也不是无符号,但可以显式的指定。
9. 十进制、二进制、十六进制与八进制
(1)十进制:以10为底,用0-9之间的数字进行表达;
(2)二进制:以2为底,用0和1来表达,计算机的最终数据存储方式;
(3)八进制:以8为底,用0-7之间数字表达,c++用前缀0表示八进制,如0177(十进制127),UNIX系统常用;
(4)十六进制:以16为底,用0-9和a-f之间的数字和字母组成,c++用0x前缀表示十六进制。如0x2b3(十进制691),常用作描述内存地址等。
10. 数据类型注意点
(1)两个整数相除,结果取整数位;
(2)指明ACSII条件下,char a = 65 和 char a = ‘A’ 才等价。
11. const与#define,及typedef
11.1 const区别:
(1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
(2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
(4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。
11.2 const优势:
(1)明确指定数据类型;
(2)有明确的作用域;
(3)可将const用于更复杂的类型, 如数组、结构体。
11.3 typedef 与#define区别:
(1)typedef有自己的作用域;#define没有作用域的限制。
(2)typedef是关键字,在编译时处理,有类型检查功能;#define是C语言中定义的语法,是预处理指令,不作正确性检查。
(3)typedef用来定义类型的别名,起到类型易于记忆的功能;#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
12. 几种指针
(1)数组的指针:c++把数组名称当成指向首元素地址的指针。
short num[5]; cout << num <<" "<< &num << endl;
以上语句在数字上来看地址相同,但num(&num[0])是一个2字节的地址,&num是一个10字节的地址。num+1是将地址加2,&num+1是将地址加10。
(2)字符串数组的指针:c++把字符串数组名称当成指向首元素地址的指针,其他同数组。
(3)结构体的指针:
struct T{}; T t;
T * pr = &t;
(4)结构体数组:
T t[3]; //结构体的数组,数组名称指向首元素的地址,t[0]. …
(5)结构体的指针数组:
T t1, t2, t3;
const T * pr[3] = {&t1, &t2, &t3}; // pr[0]->…
(6)结构体的指针的数组的指针
const T ** ppr = pr; // (*ppr)-> ....auto ppr = pr;
(7)创建动态数组
int * num = new int[n]; // n为运行时用户输入
delete [] num;
(8)多维数组的指针
int a[2][3];
int *p[3] = a; // 等价于p = &a[0][0] ,二维数组指针
int a[2][3][4];
int *p[3][4] = a; // 等价于p = &a[0][0][0], 三维数组指针
(9)函数的指针
func(func1); // 参数为func1的地址
func(func1()); // 参数为func1的返回值
double (*p) (int); //函数的指针,传入参数为int型,返回值为double型。
double *p (int) // 函数,传入参数为int型,返回一个指向double的指针。
(10)函数的指针的数组的指针
const double *f1(const double *, int); // 函数f1,返回一个指向double型的指针。
const double *(*p1)(const double *, int) = f1; // 指向函数f1的指针。
const double *(*p[3])(const double *, int) = {f1, f2, f3}; // 函数的指针的数组。
const double *( *( *pr)[3])(const double *, int) = &p; // 函数的指针的数组的指针。
(11)指向常量的指针与指针型常量
int age = 36; const int *p = &age; // p指向const int,并不代表age为常量,表示age值不可改,但p本身的值可以改。
int * const p = &age; // p指向int型指针,且p本身为const不可改,即存储位置固定,但指向的值age可以改。
const int * const p = &age; // p本身和指向的值都不可改。
13. 指针的几种骚操作
(1)一般,cout指针则打印地址,但指针类型为char*时,cout打印指针指向的字符串;
(2)int * p; p = 0xB8000000; // 赋值错误,应为p = (int *) 0xB8000000;
(3)int * p1, p2; // p2为int型, p1为指向int的指针。
(4)int * p; *p = 22333; // 避免这么做,可能会有bug,指针在使用前初始化。
(5)int a1[5]; a1[-2] = 20; // a1[-2]等价于 *(a1-2),a1指向的地址向前移2个int元素,存入20。
(6)cout<<(int *)“Hello World!”; // 打印后面字符串首元素的地址,即“H”的地址。
(7) *“abc”,输出"a"。
(8)“abc”[2],输出“c”。
(9)double x = 0.5; int *pi = &x; long & ri = x; // 不行,c++不允许一种类型的地址赋给另一种类型的指针, 也不允许一种类型的引用指向另一种类型。
14. 超复杂指针判读
右左法则:先从未定义的字符开始看,先往右看,再往左看,知道读到括号为止,然后跳出括号,再重复。
int *(*p)(int *)[5];
(*p)为函数的指针,该函数参数为指向int型的指针,该函数返回值指向有5个int型元素的指针。
15. 数组退化问题
数组作参数传递时,会退化为指向首元素的指针。
解决方法有二:(1)使用引用;(2)同时传入数组长度。
16. new与malloc区别
(1)是否需要头文件
new/delete是C++关键字,需要编译器支持。
malloc/free是库函数,需要头文件支持。
(2)是否指定大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。
而malloc则需要显式地指出所需内存的尺寸。
(3)返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
(4)分配失败时的处理
new内存分配失败时,会抛出bac_alloc异常。
malloc分配内存失败时返回NULL。
(5) 自定义类型
new会先调用operator new函数,申请足够的内存。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
(6)重载
C++允许重载new/delete操作符,如定位new,不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。
而malloc不允许重载。
(7)内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间。
而malloc函数从堆上动态分配内存。
17. 静态联编与动态联编
(1)静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用与执行该操作代码间的关系,比如数组大小、函数参数类型等等。优点是效率高,但灵活性差
(2)动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数、确定动态数组长度等。优点是灵活性强,但效率低。
18. c++ 3种内存管理方式
(1)自动存储:函数内部定义的常规变量,函数调用时产生、函数结束时消亡,存储在栈中,后进先出;
(2)静态存储:整个程序执行期间都存在的存储,如在函数外定义或函数内加static,存储在静态变量内存中;
(3)动态存储:new和delete提供的灵活存储方法,存储在堆或自由存储空间,可以在一个函数中new,在另一个函数中delete。
19. c字符串与c++字符串
(1)c字符串:以空字符结尾‘\0’,计算长度需要将空字符计算在内。char型之间不能直接赋值、比较,需要用strcpy和strcmp函数。
char a[5] = {'a', 'b', 'c' , 'd, '\0''};
char a[5] = "abcd";
char b[5];
strcpy(b, a);
(2)c++字符串:string类。string之间可以直接赋值、比较。
string a = "abcd";
string b = a;
20. 结构体与共同体
结构体可以同时存储不同类型数据;
union每次只能存储不同类型数据中的一种。
21. 逗号运算符
var = (count=19, incr=10, count+1); // 首先把 count 赋值为 19,把 incr 赋值为 10,然后把 count 加 1,最后,把最右边表达式 count+1 的计算结果 20 赋给 var。
int y = 1, 024; // y值为1。
22. 文本读写
(1)写文本:
#include <iostream>
#include <string>
#include <cstring>
#include <fstream>
using namespace std;
int main()
{string t1;char t2;ofstream outFile;string filename = "test.txt";outFile.open(filename);if (!outFile.is_open) {} // 打开失败时的处理while((cin >> t2) t2 != 'q') // while(geline(cin, t1)){outFile << t2 << endl; cin.get();}outFile.close();return 0;
}
(2)读文本:
#include <iostream>
#include <string>
#include <cstring>
#include <fstream>
using namespace std;
int main()
{string t1;char t2;ifstream inFile;string filename = "test.txt";inFile.open(filename);if (!inFile.is_open) {} // 打开失败时的处理while(!inFile.eof) // 一直读到最后{outFile >> t2;}outFile.close();return 0;
}
23. break与continue
continue直接进入下次循环;
break直接跳出循环。
24. 函数传参与返回值原理
(1)函数按值传参:
实际上是复制了一个外部变量的副本进入函数中,在函数内操作的为该副本,对该变量的修改并不会实际修改函数外的该变量。
(2)函数引用传参:
引用相当于是外部变量的别名,实际操作的就是该变量,即在函数内对该变量进行修改的话,在外部该变量也会相应被修改。
(3)函数返回值:
函数将返回值复制到指定的CPU寄存器或内存单元中,调用程序查看该单元,判断数据类型是否一致,一致则获取返回值。返回值为引用时,不可返回函数内部的局部变量的引用。
25. 常规函数与内联函数工作原理
常规函数:常规函数调用也使程序调到另一个地址,并在函数结束时返回。来回跳跃并记录跳跃位置需要开销;
内联函数:编译器使用相应的函数代码代替函数调用。运行速度快,但消耗更大内存。比如inline修饰或在类的声明中的函数实现。
26. 函数模板
(1)函数模板的声明及定义
template <typename T>
void fun(T a){}
(2)函数模板的重载
template <typename T>
void fun(T a);
template <typename T>
void fun(T *a, int b);
(3)函数模板的显式具体化
函数特征标一样(无法重载)、目的一样(最好还是用这个函数模板)、但是局部功能的实现不一样,此时就需要模板的显式具体化。
(4)函数模板的显式实例化
在调用模板时明确指出生成特定数据类型的函数。
(5)函数模板的隐式实例化
在调用函数模板时提供特定数据类型的参数。
27. c++函数匹配的原则(这么多重载和泛型用哪个)
(1)创建候选函数表;
(2)根据参数数目和类型,创建可行函数表;
(3)选用最佳可行函数。
28. 4种c++变量生命周期(存储持续性)
(1)自动存储持续性:函数中声明的变量,函数执行时被创建,函数执行完时内存被释放;
(2)静态存储持续性:函数外定义或函数内用static修饰的变量,程序运行全过程存储;
(3)线程存储持续性:用thread_local修饰,生命周期与所属线程一样长;
(4)动态存储持续性:用new分配的内存变量,直到delete释放内存。
29. 5种c++变量存储方式(内存分配方法)
变量作用域:变量名称在多大的范围内可见。
变量链接性:变量名称如何在不同单元间共享,外部(文件间)、内部(文件内)、无链接。
30. 变量的限定符、CV限定符
cv限定符:const;volatile,可被硬件、系统、线程等编译器未知因素更改。
(1)auto:修饰自动变量,新版为自动类型推断;
(2)register:修饰寄存器存储变量,新版用来指定自动变量;
(3)static:静态变量;
(4)extern:引用声明,引用其他地方定义的变量,常与外部链接性static变量搭配;
(5)thread_local:指出变量持续性与其所属线程持续性相同;
(6)mutable:指出即使用const修饰结构体或者类,用mutable修饰的某个成员也可以被修改。
31. using声明与using定义
using声明:using namespace std; // 函数外,全局作用
using定义: 有作用域
namespace n1
{int y = -4;} // 单个文件中定义,或在函数中指明"using namespace n1;"
32. 类的特点(结合Q2)
(1)抽象:以类方法的公有接口操作类对象;
(2)封装:类额数据成员可私有,只能通过类的成员函数来访问;
(3)隐藏:类的数据表示和方法这些具体细节都是隐藏的。
33. c++类与结构体的区别
c++对结构体进行了扩展,使之具有与类相同的特性。
结构体默认访问的类型是public;类默认为private。
34. this指针
比如类的两个对象做比较,返回较大的。
class T{};T a, b;
可在T中定义一个比较函数:
const T & max2(const T & t) const; // 传入参数为T的对象,返回值也为T的对象,最后一个const表明该函数不会修改被隐式访问的对象。
const T & max2(const T & t) const{if (t.num > num)return t;elsereturn *this;}
35. 类默认创建的方法
class T{};
(1)默认构造函数:T(){}
(2)默认析构函数:~T(){}
(3)复制构造函数
(4)赋值运算符
(5)地址运算符
36. 析构函数的作用域
(1)无说明为整个main函数;
(2)main函数中加{}限定,作用域为{}的代码块范围。
37. 类的对象的数组
class T{};
T t[4] = {T(...), T(...), T(...), T(...)}; // 初始化,每个对象调用一次构造函数
38. 类中运算符重载的3种情况
class T{};
(1)类的成员函数:T operator+(const T &t),这种重载只能——对象(运算)对象;
(2)友元函数:为了满足诸如乘法交换律等需要,friend T operator*(int x, const T & t), 这种重载支持——对象(运算)非对象;非对象(运算)对象;
(3)cout重载:为了实现连续打印,ostream &operator<<(ostream & os, const T & t);
39. 运算符重载的限制
(1)重载后的运算符至少有一个操作数是用户定义的类型,如将减号重载为计算和;
(2)不能违反原来额语法规则和优先级,如不能将取模运算重载为只有一个操作数;
(3)不能创建新运算符,如用**表示求幂;
(4)只能通过成员函数重载的:=、()、[]、->;
(5)不能重载的:sizeof、.、.*、::、?:、typeid、const_cast、dynamic_cast、reinterpret_cast、static_cast。
40. 友元函数与成员函数的区别
成员函数是类定义的一部分,通过特定对象来调用,成员函数可以隐式访问对象的成员,无需成员运算符.;
友元函数不是类的组成部分,直接函数调用,不能隐式访问对象成员。
41. 深拷贝与浅拷贝
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。当原值内存释放时,浅拷贝的值为空,且新值再次释放内存时会报错;
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
42. 类中使用指针,必须要显式定义的方法
class T{char * str;...};
(1)自定义构造函数:
T::T(const char* a)
{str = new char[strlen(a)+1];strcpy(str, a);
}
(2)拷贝构造函数:
T::T(const T & t)
{str = new char[strlen(t.str)+1];strcpy(str, t.str);
}
(3)赋值运算符重载:
T & T::operator=(const T & t)
{if (this == &t) {return *this;}delete [] str;str = new char[strlen(t.str)+1];strcpy(str, t.str);return *this;
}
(4)析构函数
T::~T()
{delete [] str;
}
43. 类中静态变量的初始化及静态方法和静态成员访问
(1)不能在类额声明中初始化静态成员。
class T{static int a; …}; // 类的声明。
int T::a = 0; // 使用类时在主函数外初始化。
(2) 引用静态数据成员时,采用如下格式:
<类名>::<静态成员名>
(3)静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。对静态成员的引用不需要用对象名。在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:
<类名>::<静态成员函数名>(<参数表>);
44. 类的对象的指针
class T{}; T t;
(1)常规方法声明
T * t1;
(2)将指针初始化为已存在的对象
T *t1 = &t[0];
(3)new初始化指针
T *t = new T(t[0]);
(4)调用类的构造函数初始化
T *t = new T("…");
45. 类的对象初始化、复制与赋值综合判断
class T{};
(1)T t1; // 默认构造函数
(2)T t2 (" … "); // 自定义构造函数
(3)T *p = new T; // 隐式调用默认构造函数
(4)T t3 = t1; // 拷贝构造函数
(5)*p = t1; // 赋值函数
(6)t1 = " … "; // 先自定义构造函数,再赋值函数
(7)t1 = t2; // 赋值函数
(8)T t4 = T(); // 显式调用默认构造函数
46. 派生类可以从基类继承什么
(1)公有成员继承为公有成员;
(2)保护成员继承为保护成员;
(3)私有成员不能由派生类直接访问。
47. 派生类不可以从基类继承什么
不能继承构造函数、析构函数、赋值运算符和友元函数。
48. c++ 类的3种继承方式
(1)普通继承:不改变基类成员和方法,可以新增成员和方法。
(2)虚函数继承:改变基类方法的实现,两边成员函数都用virtual修饰。要使用虚的析构函数确保析构顺序正确。
(3)纯虚函数继承:改变基类的成员和方法实现,virtual … = 0。纯虚函数的类只用作基类,该基类中的方法在派生类中单独实现。
49. 派生类需要重新定义的方法
class T{int a;};
class t:public T{int b;};
(1)默认构造函数:
t::t(int x, const T & p):T(p)
{b = x;
}
(2)自定义构造函数
t::t(int x, int y):T(y)
{b = x;
}
(3)析构函数
t::~t(){}
50. 虚函数的工作原理及限制
(1)编译器给每个对象添加隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚函数表。表中存储了为类对象进行声明的虚函数地址。
(2)调用虚函数时,程序先查看虚函数表地址,然后转向虚函数表。从表中选择对应声明顺序的虚函数地址,并执行函数。
(3)虚函数在内存和执行速度上有成本:每个对象增大(存储虚函数表的地址)、对每个类编译器都要创建虚函数地址表、每个函数的调用都要额外到虚函数地址表表中查找函数地址。
51. 基类用new分配内存的情况(参考Q42类中有指针的情况)
(1)派生类不用new:无需显式定义派生类的析构函数、拷贝函数和赋值运算符;
(2)派生类中有新的new:
class T{char *p; ...}
class t:public T{char *pr; ...}
显式定义派生类的构造函数:
T::T(const char *a)
{p = new char[strlen(a)+1];strcpy(p, a);
}
t::t(const char *a, const char *b):T(a)
{pr = new char[strlen(b)+1];strcpy(pr, b);
}t::t(const char *a, const T & b):T(b)
{pr = new char[strlen(a)+1];strcpy(pr, a);
}
显式定义派生类的析构函数:
T::~T(){delete [] p;}
t::~t(){delete [] pr;}
显式定义派生类的拷贝构造函数:
T::(const T & a)
{p = new char[strlen(a.p)+1];strcpy(p, a.p);
}
t::t(const t & b):T(b) // 先拷贝基类T的成员
{pr = new char[strlen(b.pr)+1];strcpy(pr, b.pr);
}
显式定义派生类的赋值运算符:
T & T::operator=(const T & a)
{if(this == &a){return *this;}delete [] p;p = new char[strlen(a.p)+1];strcpy(p, a.p);return *this;
}
t & t::operator=(const t & b)
{if (this == &b){return *this;}T::operator=(b); // 先赋值基类T的成员delete [] pr;pr = new char[strlen(b.pr)+1];strcpy(pr, b.pr);return *this;
}
52. protected修饰符对于继承的意义
无派生关系时,protected与private相似,类外只能用公有的方法访问。
派生类的成员可以直接访问基类protected成员,但不能直接访问基类private成员。
53. 基类与派生类的指针和引用
(1)基类指针可以指向派生类,即可以给基类指针赋派生类的地址;
(2)基类引用可以引用派生类,即可以给基类的对象赋派生类的对象。
54. 类的包含与私有继承的区别
(1)类的包含:通过调用被包含的对象来调用被包含的类的方法:
class T
{ ... void show(); ...
};class t
{... T a; ... a.show();...
};
(2)私有继承:通过基类类名引用来调用基类方法:
class T
{ ... void show(); ...
};class t:private T
{... T::show();...
};
55. 模板类
template <class T>
class t
{...T a;...
}
方法实现:
template <class T>
t<T> ::t()
{...
}
56. c++处理异常的方法
(1)调用abort()函数,终止整个程序的运行;
(2)返回错误代码;
(3)try-throw-catch:
int main()
{try // 执行包含throw的正常操作代码{error();}catch(const char *s) // 调用catch处理异常后的操作{cout << s << endl;cout << "下一步处理"<<endl;}
}void error()
{if ("符合异常的条件"){throw“出现了具体什么情况的异常”;// throw触发异常}cout << "输出正常结果"<<endl;
}
这篇关于重学c++ Primer Plus纪要(第1-15章)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!