C++知识点总结(上)《C++ Primer》
文章目录
参考教材:《C++ Primer》(Stanley B. Lippman 第五版)
2020.9.6
By 盒子先生KingO
一、编译流程
源代码(source coprede)→预处理器(processor)→编译器(compiler)→汇编程序(assembler)→目标程序(object code)→链接器(Linker)→可执行程序(executables)
1、预处理
- 读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理
- 删除所有的注释
- 添加行号和文件名标识
- 保留所有的#pragma编译器指令
2、编译
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
3、汇编
将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
4、链接
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
将生成的.obj文件与库文件.lib等文件链接,生成可执行文件(.exe文件)
二、常识
1、windows下
- .dll:动态链接库,作为共享函数库的可执行文件
- .obj: 对象文件,相当于源代码对应的二进制文件,未经重定位
- .lib: 可理解为多个obj的集合,本质与.obj相同
2、linux下
- .so:(share object)动态链接库,跟Windows平台类似
- .o: 对象文件,相当于源代码对应的二进制文件
- .a: 与.o类似,多个.o的集合
3、C和C++的一个区别
c++的编程思想是面向对象,而c的编程思想是面向过程
-
“自顶向下,逐步求精”的面向过程程序设计
面向过程程序设计的思想即这样的一种解决思路 - 提出问题,分析问题的处理流程,将大问题分解为小问题,如果小问题比较复杂,就继续划分小问题为更小的问题,然后通过小模块一一解决小问题,最后再根据整个业务流程将这些小问题串在一起(调用函数),这样就达到了解决所有问题的目的
- 优点:
- 程序结构简单(仅由顺序、选择、循环构成)
- 分而治之,逐个击破
- 自顶向下,逐步求精
- 缺点:
- 数据和操作往往是分离的(数据结构发生变化,操作的函数不得不重新改写)
- 数据往往不具有封装性,变量暴露在全局
- 优点:
-
万般皆对象:面向对象程序设计
多态、继承、封装
面向对象思想认为:现实世界是由对象组成的,无论大到一个国家还是小到一个原子,都是如此。并且对象都由两部分组成 - 描述对象状态或属性的数据(变量)以及描述对象行为或者功能的方法(函数)。并且与面向过程不同,面向对象是将数据和操作数据的函数紧密结合,共同构成对象来更加精确地描述现实世界,这是面向过程和面向对象两者最本质的区别。
- 优点
- 容易设计和实现
- 复用设计和代码,开发效率和系统质量都得到了提高
- 容易扩展
- 安全性高
三、重要知识点
1、引用
引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。
int ival = 1024;
int &refVal = ival; //refVal 指向ival(是ival的另一个名字)
int &refVal2; //报错:引用必须被初始化
定义引用时,程序把引用和它的初始值**绑定(bind)**在一起,而不是拷贝。引用即别名,并非对象
int &refVal3 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal4 = dval; //错误:此处引用类型的初始值必须是int型对象
2、指针
指针,本身是一个对象,允许赋值和拷贝,无需在定义时赋初值。
int ival = 42;
int *p = &ival; //正确
double *pd1 = &ival //错误:类型不匹配
空指针:nullptr
int *p1 = nullptr; //C++新标准
//等价于
int *p1 = 0;
建议:初始化所有指针(如果不初始化,将拥有一个不确定的值)
int ival = 1024;
int *p = &ival;
int **ppi = π //指向指针的指针
int i = 42;
int *p;
int *&r = p; //指向指针的引用
3、const
const类型的对象不能改变其内容
默认状态下,const对象仅在文件内有效,如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern
//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize; //与file_1.cc中定义的bufSize是同一个
程序员常将**“对const的引用"简称为"常量引用”**
引用的类型必须与其所引用对象的类型一致,但有两种特殊情况。其中之一,常量引用时允许用任意的表达式作为初始值
int i = 42;
const int &r1 = i; //允许将const int &绑定到一个普通int对象上
const int &r2 = 42; //正确:r2是一个常量引用
//reason: 编译器会创建一个临时量对象
//
const int r = 42;
const int &r2 = r;
//
const int &r3 = r1 * 2; //正确:r3是一个常量引用
int &r4 = 42: //错误
int &5 = r1*2; //错误,非常量引用不可以这么干
常量指针(const pointer),必须初始化,一旦初始化完成,则值不能再改变(存放在指针中的地址),不变的是指针的本身而不是指向的那个值
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π //pip是一个指向常量对象的常量指针
- **顶层const:**指针本身是个常量
- **底层const:**指针所指的对象是一个常量
4、static
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
5、this指针
-
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象 -
当对一个对象调用成员函数时,编译程序先将对象的地址赋给
this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用this
指针。 -
当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
-
this
指针被隐含地声明为:ClassName *const this
,这意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作); -
this
并不是一个常规变量,而是个右值,所以不能取得this
的地址(不能&this
)。 -
在以下场景中,经常需要显式引用
this
指针:- 为实现对象的链式引用;
- 为避免对同一对象进行赋值操作;
- 在实现一些数据结构时,如
list
。
6、using声明
得到命名空间中的成员 using namespace::name;
#include<iostream>
#include<string>
using std::cin;
using std::cout;
using std::string;
头文件中不应包含using声明
7、string
string s1; //默认初始化,s1是一个空字符串
string s2 = s1; //s2是s1的副本
string s3 = "hello"; //s3是该字符串字面值的副本
//等价于
string s33("hello");
string s4(10,'C'); //S4的内容是cccccccccc
//读取未知数量的string对象
string word;
while(cin>>word)
cout<<word<<endl;
//使用getline读取一整行
string line;
//每次读入一整行,直至到达文件末尾
while(getline(cin,line))
cout<<line<<endl;
//每次读入一整行,遇到空行直接跳过
while(getline(cin,line))
if(!line.empty())
cout<<line<<endl;
//每次读入一整行,输出其中超过80个字符的行
while(getline(cin,line))
if(line.size()>80)
cout<<line<<endl;
string s = s1 + "," + "world" + '\n';
//处理每个字符
string str("some string");
for(auto c : str)
cout<<c<<endl;
字面值和string对象相加
当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string:
string s2 = s1 + ","; //正确:把一个string对象和一个字面值相加
string s3 = "hello"+","; //错误:两个运算对象都不是string
string s4 = s1 + "," + "word"; //正确:每个加法运算符都有一个运算对象是string
//reason: (s1+",")+"word"
string s5 = "hello" + "," + s1; //错误:不能把字面值直接相加
//reason: ("hello"+",") + s1
8、vector
vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引。 容器、类模板
//列表初始化vector对象
vector<string> articles = {"a","an","the"};
vector<string> v1{"a","an","the"};
vector<string> v1("a","an","the"); //错误
//push_back添加元素
vector<int> v2; //空vector对象
for(int i = 0; i != 100; ++i)
v2.push_back(i);
9、迭代器
访问string对象的字符或vector对象的元素,除了使用下标运算符外,还有一种更通用的机制——迭代器(iterator)。这些类型拥有名为begin和end的成员。begin成员返回指向第一个元素的迭代器,end成员负责返回指向容器“尾元素的下一个位置(one past the end)”的迭代器(也就是本不存在)。如果容器为空,则begin和end返回的是同一个迭代器
string s("some string");
for(auto it = s.begin; it != s.end() && !isspace(*it); ++it)
*it = toupper(*it);
泛型编程:C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标地原因一样:因为这种编程风格在标准库提供的所有容器上都有效。(for循环中使用!=而非<进行判断)
//使用迭代器完成二分搜索
auto beg = text.begin(),end = text.end();
auto mid = text.begin() + (end - beg) / 2;
while(mid != end && *mid != sought)
{
if(sought < *mid)
end = mid;
else
beg = mid + 1;
mid = beg + (end - beg) / 2;
}
10、sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数,其所得值是一个size_t类型的常量表达式。其运算对象有两种形式: 1.sizeof(type)
2.sizeof expr
Sales_data data, *p;
sizeof(Sales_data); //存储Sales_data类型的对象所占的空间大小
sizeof data; //data的类型的大小,即sizeof(Sales_data)
sizeof data.revenue; //Sales_data得revenue成员对应类型的大小
sizeof Sales_data::revenue; //另一种获取revenue大小的方式
sizeof运算符的结果部分地依赖于其作用的类型:
- 对char或者类型为char的表达式执行sizeof运算,结果得1
- 对引用类型执行sizeof运算得到被引用对象所占空间的大小
- 对指针执行sizeof运算得到指针本身所占空间的大小
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效
- 对数据执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理
- 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中元素占用了多少空间
11、try语句块和异常处理
- throw表达式:异常检测部分使用throw表达式来表示它遇到了无法处理的问题
- try语句块:异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句“处理”异常,所以它们也被称作异常处理代码
- 异常类:用于在throw表达式和相关的catch子句之间传递异常的具体信息
throw表达式
if(item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN"); //如果程序执行到这里,发生异常
//异常类型为runtime_error的对象,抛出异常将终止当前的函 //数,并把控制权转移给能处理该异常的代码
try语句块
通用语法形式:
try{
program-statements //正常逻辑
} catch (exception-declaration){ //异常声明1
handler-statements
} catch (exception-declaration){ //异常声明2
handler-statements
} // ...
常见异常类
exception | 最常见的问题 |
---|---|
runtime_error | 只有在运行时才能检测出的问题 |
range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
logic_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的结果值不存在 |
invalid_argument | 逻辑错误:无效参数 |
length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |
12、函数
**内联函数:**可避免函数调用的开销
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
// 声明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函数的处理步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
优缺点
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
**assert预处理宏:**所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:
assert(expr)
首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行,如果表达式为真(即非0),assert什么也不做。
**NDEBUG预处理变量: **assert的行为依赖于NDEBUG预处理变量的状态,如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
我们可以使用一个#define
语句定义NDEBUG,从而关闭调试状态。
**C++编译器定义的静态数组:**方便用于程序调试
_ _ func _ _ | 存放当前调试的函数的名字_ |
---|---|
_ _ FILE _ _ | 存放当前文件名的字符串字面值 |
_ _ LINE _ _ | 存放当前行号的整型字面值 |
_ _ TIME _ _ | 存放文件编译时间的字符串字面值 |
_ _ DATE _ _ | 存放文件编译日期的字符串字面值 |
**函数指针:**函数指针指向函数而非对象
//比较两个string对象的长度
bool lengthCompare(const string &, const string &);
//声明一个可以指向该函数的指针
bool (*pf)(const string& , const string&);
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价的赋值语句:取地址符是可选的
bool b1 = pf("hello", "goodbye"); //调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); //一个等价的调用
13、union联合
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
-
默认访问控制符为 public
-
可以含有构造函数、析构函数
-
不能含有引用类型的成员
-
不能继承自其他类,不能作为基类
-
不能含有虚函数
-
匿名 union 在定义所在作用域可直接访问 union 成员
-
匿名 union 不能包含 protected 成员或 private 成员
-
全局匿名联合必须是静态(static)的
#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; }
14、::范围解析运算符
分类:
- 全局作用域符(
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
int count = 11; // 全局(::)的 count
class 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;
}
14、::范围解析运算符
分类:
- 全局作用域符(
::name
):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间 - 类作用域符(
class::name
):用于表示指定类型的作用域范围是具体某个类的 - 命名空间作用域符(
namespace::name
):用于表示指定类型的作用域范围是具体某个命名空间的
int count = 11; // 全局(::)的 count
class 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;
}