二、程序结构
1. 循环
-
x++和++x
-
测试用户输入合法性时可以使用do while语句
-
读取char值时,cin会忽略掉空格和回车,不愿忽略可以使用
cin.get(ch); // ch实时改变
结束可以用cin.eof()
或cin.fail()
,但是对于键盘输入来说可以用cin.clear();
清除标记继续读取(部分系统)while (cin.get(ch)){ ... }
-
逗号的一些情况示例(优先级最低)
cats = 17, 240; // 等价于(cats = 17), 240;赋值为17 cats = (17, 240); // 赋值为240
2. 分支、逻辑运算符
-
C++中也可以用not、and、or,方法和python里一样
-
字符函数库cctype
#include<cctype> isalpha(); isdigit(); isspace(); // 检测是否为空白' ', '\n', '\t' ispunct(); tolower(); // 如果是大写字符,则返回其小写,否则返回该参数 toupper(); // 如果是小写字符,则返回其大写,否则返回该参数 isupper(); // 如果是大写字母,返回true
-
if-else和switch都能使用时,如果选项大于等于3个,用switch更快
-
int类型cin入一个字母会怎么样?
int n; cin >> n;
- n的值保持不变
- 不匹配的输入将被留在输入队列中
- cin对象中的一个错误标记被设置
- 对cin方法的调用将返回false
而后可以用
cin.clear();
继续接收输入
3. 简单文件输入/输出
-
读取整行带空格不带回车的字符时使用
cin.getline(name, size);
-
写入到文本文件与控制台输出极为类似
需要十分注意程序的健壮性
#include<fstream> // fstream中定义了一个用于处理输出的ofstream类 #include<cstdlib> ofstream fout; char filename[50]; cin >> filename; fout.open(filename); char line[80] = "I love C++"; fout << line << endl; // 和cout一样,有<<、endl、setf()等 fout.close(); // 最后需要关闭文件
#include<fstream> // fstream中定义了一个用于处理输入的ifstream类 ifstream fin; fin.open(filename); /*********重要**********/ if (!fin.is_open()){ // 文件打开失败的情况 exit(EXIT_FAILURE); // 定义在cstdlib中 } /***********************/ fin.getline(line, 80);
什么时候文件会打开失败?
- 指定的文件可能不存在
- 文件可能位于另一个目录(文件夹)中
- 访问可能被拒绝
- 用户可能输错了文件名或省略了扩展名
-
小总结:fstream中定义的文件中有输入输出两个类,分别定义对象,然后像cin和cout一样使用即可
4. 函数
-
C++函数返回值不能是数组,但可以是其它任何类型
函数是如何返回值的?
- 函数将返回值复制到指定的CPU寄存器或内存单元中
- 调用程序查看该内存单元
- 返回函数和调用函数就该内存单元中存储的数据类型达成一致
-
传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组(首地址指针)
// 假设4字节地址系统 void func(int a[], int n){ cout << sizeof(a); // 输出的是4 } int main(){ int cookies[8]; cout << sizeof(cookies); // 输出的是32 func(cookies, 8); }
如果我们要实现数组的**“值传递”,那么可以将形参改为
const int a[]
**const和constexpr的区别
-
const并未区分出编译期常量和运行期常量
-
constexpr限定在了编译期常量
总的来说,编译期就能算出来,可以提高运行的效率,能用constexpr就用constexpr
-
-
const + 指针
禁止将const指针赋值给非const指针
-
指针指向常量**
const int * ps = &sloth;
**- 可以修改指向的对象(指向另一个对象)
- 不能通过该指针修改指向对象的值
- 仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const指针赋值给const指针
-
常量指针指向变量**
int * const finger = &sloth
**- 不能修改指向的对象(指向另一个对象)
- 可以通过该指针修改指向对象的值
-
常量指针指向常量**
const double * const stick = &trouble
**- 易得,什么都改不了
小总结:
const int *
是指向常量,所以不能该指向对象的值;int * const
是指针本身为常量,所以不能修改指向的对象(专一) -
-
二维数组传参
// 方式1,用数组指针(指向指针的指针,所以不用const) int sum(int (*arr)[4], int n); // 方式2(第二维的大小无法省略) int sum(int arr[][4], int n); // 由以上两种方法都可知:arr形参是指针,因此可以如下方式取值 int a = *(*(arr + row) + col);
5. *函数指针
函数的地址是存储其机器语言代码的内存的开始地址
-
函数的地址:函数名就是函数的地址
-
声明函数指针:
函数指针就是用
(*name)
替换掉函数原型中的函数名double pam(int); // 函数原型 // 正确函数指针声明如下 double (*pf)(int); // 函数指针,可以想到函数签名,用最少的量表明完整的一个函数 // double *pf(int); // 这只是一个返回值是double指针的函数原型 pf = pam; // 函数签名必须一样,否则无法赋值!
-
使用指针来调用函数
// 第一种方式 double y = (*pf)(5); // 虽然比较复杂,但是说明了这是个函数指针 // 第二种方式 double y = pf(5);
我们再来深究一下
-
下面三个函数的原型特征标和返回类型相同
// 在形参中,数组就是指针 // 返回值是const double *,也就是一个指向常量的指针 const double * f1(const double a[], int n); const double * f2(const double [], int); const double * f3(const double *, int); // 函数指针 const double *(*pf1)(const double *, int) = f1; // 当然,我们可以这样! auto pf2 = f2;
-
但是这样函数太多了,所以我们不妨试一试指针函数数组
// TODO(可以把所有排序算法用函数指针数组写一遍)
const double * (*pf1)(const double *, int) = f1; // 这是一个函数指针 auto pf2 = f2; auto pf3 = f3; // 或者是这样 // auto自动类型推断只能用于单值初始化,而不能用于初始化列表 // 因为这首先是个指针数组,所以*pf_array1[3]表明了指针数组 const double * (*pf_array1[3])(const double *, int); // 这是一个函数指针数组 // 当有了一个数组后,一切都简单了 auto pf_array2 = pf_array1;
那么如何调用这些函数呢?
其实和普通指针是一样的
double a = *(*pf_array1[2])(p, 3); // 对应上面double y = (*pf)(5); double b = *pf_array1[2](p, 3); // 对应上面double y = pf(5); // 最左边那个*的原因是函数返回值本身是个指针,要取值的话就得加*
// TODO 函数指针对性能有什么影响吗?
-
那么函数数组指针呢?
// 和指针数组、数组指针一个道理 const double * (*(*array1_pf)[3])(const double *, int); // 但是我选择auto auto array2_pf = &pf_array1; /* * array2_pf指向数组,那么*array2_pf就是数组中的元素,也就是函数指针; * 那么(*array2_pf)[x]就是某函数指针 * 所以,调用就是(*(*array2_pf)[x])(p, 3); * 最后,由于函数返回值是指针,取值要再加一个* */ double a = *(*(*array2_pf)[x])(p, 3);
小总结:
指针函数一个*就可以进行调用函数;
指针函数数组一个*可以调用指针函数,两个*可以调用函数;
函数指针数组一个*可以调用数组,两个*可以调用指针函数,三个*可以调用函数
还是用auto吧!
-
或者还有个选择:typedef
typedef sturct{ int x, y; }POINT; typedef const double * (*p_fun)(const double *, int); p_fun p1 = f1;
6. 函数探幽
-
内联函数
内联函数的作用是提高运行速度,它与常规函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中
内联函数采用直接替代的方式,而不是通过堆栈调用,因此虽然提高效率,但是牺牲了内存,所以采用内联的函数通常较小、但被多次调用
// 可以用来替换C中的 #define inline double square(double x){return x * x;}
-
引用变量
引用必须在声明时初始化(比较像const指针,从一而终)
int &rodents = rats; int * const pr = &rats;
如果实参与引用参数不匹配,且参数为const引用时,C++将在以下两种情况时生成临时变量
-
临时变量
这种时候会生成匿名临时变量,并让引用指向它,这些临时变量只在函数调用期间存在,最后让你函数不成功
所以现在的C++标准禁止这样创建的临时变量……(简单粗暴)
-
void swap(int &a, int &b){ int temp = a; a = b; b = temp; } // 也就是说,下面这些调用都是错误的!(已验证) double a = 1, b = 2; swap(a, b); int a = 1; swap(a + 1, b); swap(a, 2); // 但是把swap里的int换成const int就都可以了,因为const是左值!
应尽可能使用const
这里我们再次强调const的重要性!
- 使用const可以避免无意中修改数据的编程错误
- 使用const使函数能够处理const和非const实参,否则将只能接受非const数据
- 使用const引用使函数能够正确生成并使用临时变量
-
右值引用(C++11新特性)
右值区别于上文说到的左值,那么上面的引用便称为左值引用,右值引用使用符号&&
double &&rref = sqrt(36.00); int j = 15.0; double &&jref = 2.0 * j + 18.5;
右值引用可以实现移动语义,这在之后会说到
-
引用用于结构
(函数返回引用有什么用?和返回值不一样吗?返回值会执行一次拷贝!地址是和原值不一样的!)
// 另外,考虑这个情况 POINT &accumulate(POINT &p1, const POINT &p2){ p1.x += p2.x; p1.y += p2.y; return p1; } // 为了实现累加操作,我们必须返回引用(类似于cin >> a >> b;) p1 = accumulate(accumulate(p1, p2), p3); // 这种情况如果返回的不是引用就会报错!
但是也不能因此就全部返回引用!
// 返回引用出错了! int & sum(const int a, const int b){ return a + b; } // 也就是说,不能返回右值(无法引用的对象),当然也不能返回局部变量,因为函数结束就没了 int & sum(const int a, const int b){ int sum = a + b; return sum; }
-
对象、继承和引用
接收基类引用作为参数的函数,调用该函数时,也可以将派生类对象作为参数**(基类引用可以指向派生类对象)**
关于ios_base::的一点东西
setf(ios_base::fixed); // 将对象置于使用定点表示法的模式 setf(ios_base::showpoint); // 将对象置于显示小数点的模式,即使小数部分为0 precision(); // 指定显示多少位小数(定点模式下) width(); // 设置下一次输出字段宽度 ios_base::fmtflags initial; initial = os.setf(ios_base::fixed); // 保存字体格式到initial中 ... os.setf(initial); // 重新设置为initial的字体格式
什么时候使用引用参数(指导原则)
- 对于使用传递的值而不做修改时
- 如果数据对象很小,如内置数据类型或者小型结构,按值传递
- 如果数据对象是数组,则使用指针,并将指针声明为const的指针
- 如果数据对象是较大的结构,则使用const指针或者const引用,以提高程序的效率,节省复制结构所占用的时间和空间
- 如果数据对象是类对象的话,则使用const引用
- 对于修改调用函数中数据的函数
- 如果数据对象是内置数据类型,则使用指针
- 如果数据对象是数组,则只能使用指针
- 如果数据对象是结构,则使用引用或者指针
- 如果数据对象是类对象的话,则使用引用
- 对于使用传递的值而不做修改时
下面是一些比较重要的类型
-
函数重载
参数引用和非引用无法重载
const和非const可以重载(非const可以用const的函数)
返回值不一样不算重载
名称修饰
C++如何跟踪每一个重载函数?用名称修饰(名称矫正)
long MyFunctionFoo(int, float);
经过编译器会转换为内部表示,来描述该接口
?MyFunctionFoo@@YAXH
-
函数模板
常将模板放在头文件中,并在需要使用模板的文件中包含头文件
在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案
模板在函数声明上要有一个,在函数上也要有一个
template <typename AnyType> // 也可以用template <class AnyType> void swap(AnyType &a, AnyType &b){ // 更常用template <typename T> AnyType temp = a; a = b; b = temp; }
重载的模板
template <typename T> void swap(T &a, T &b); template <typename T> void swap(T *a, T *b, int a);
但是模板无法应对一些问题,如结构体之间不能比较大小,数组不能赋值,指针的地址比较可能不是想要的结果等,解决方法有:① 重载运算符 ② 为特定类型提供具体化的模板定义
模板显式具体化
- 对于给定的函数名,可以有非模板函数、模板函数和具体化模板函数以及它们的重载版本
- 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型
- 非模板函数优先于具体化,具体化优先于常规模板
// 非模板 void swap(job &, job&); // 模板 template <typename T> void swap(T &a, T &b); // 具体化(是在有模板的基础上规定模板的T到底是什么) template <> void swap<job>(job &, job &);
实例化和具体化
模板需要调用!模板需要调用!模板需要调用!
编译器使用模板为特定类型生成函数定义时,得到的是模板实例
-
隐式实例化:函数调用
swap(i, j)
,编译器通过传入类型知道怎么定义 -
显式实例化:直接命令编译器
template void swap<int>(int, int);
-
显式具体化(需要自己实现函数):
// 二者等价 template <> void swap<job>(job &, job &); template <> void swap(job &, job &); // 函数实现 template <> void swap<job>(job &j1, job &j2){ double temp = j1.salary; j1.salary = j2.salary; j2.salary = temp; }
*隐式实例化*:使用模板之前,编译器不生成模板的声明和定义示例,后面有程序用了,编译器才会根据模板生成一个实例函数
*显式实例化*:是无论是否有程序用,编译器都会生成一个实例函数(有什么用?)
*显示具体化*:因为对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时就是使用显示具体化的场景
(为什么形参不能用auto自动识别呢?)
那么编译器将选择使用哪个函数版本呢?
- 创建候选函数列表(包含与被调用函数的名称相同的函数和模板函数)
- 使用候选函数列表创建可行函数列表(都是参数数目正确的函数,不考虑返回值类型)
- 确定是否有最佳的可行函数(如果有,调用,否则出错)
- 接下来,按下面顺序匹配
- 完全匹配,但常规函数优先于模板
- 提升转换(char和short自动转换为int,float自动转换为double)
- 标准转换(int转换为char,long转换为double)
- 用户定义的转换,如类声明中定义的转换
完全匹配和最佳匹配
表 完全匹配允许的无关紧要转换
从实参 到形参 Type Type& Type& Type Type[] * Type Type(argument-list) Type(*)(argument-list) Type const Type Type volatile Type Type * const Type Type * volatile Type * 如果有多个匹配的原因,编译器将无法完成重载解析过程
但也有例外
- 指向非const数据的指针和引用优先与非const指针和引用参数匹配
- 如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先(显式具体化优于隐式生成),“具体”指执行的转换最少
- 可以自己指定,如
less<>(m, n);
表明一定用的模板函数 - 当函数有多个参数时会很复杂,不讨论
-
模板函数的发展
decltype(x) y;
C++11引入的关键字,让y的类型和x一样(x可以是表达式)template <class T1. class T2> // 由于不知道T1,T2类型,返回值类型也不好确定;也不能用decltype,因为这在x,y作用域外 // 因此可以后置返回类型 auto gt(T1 x, T2 y) -> decltype(x + y){ return x + y; }
7. 内存模型和名称空间
-
单独编译(翻译单元,translation unit)
只修改了一个文件的话,只需要重新编译该文件,并将它和其他文件的编译版本链接(UNIX和Linux都具有make程序)
不要将函数定义或变量声明放在头文件中
头文件常含内容
- 函数原型(声明)
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数(inline)
// 防卫措施 #ifndef HEAD #define HEAD ... #endif
不同编译器可能采用不同名称修饰(把函数名变为?MyFunctionFoo@@YAXH等),因此链接不同库文件时可能会出错,这时如果有源代码的话,需要重新编译
-
存储持续性、作用域、链接性
-
存储持续性
静态变量3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)、无链接性(只能在当前函数或代码块中访问)
// 未被初始化的静态变量所有位都被设置为0 int global = 1000; // 外部链接性 static int one_file = 50; // 内部链接性 void func(){ static int count; // 无链接性 }
-
(尽量不用全局变量),如果要从其他文件调用,需要extern声明
在全局变量和局部变量同名的情况下,用
::
加上变量名可以调用全局变量 -
static内部链接性变量会隐藏外部链接性变量
-
static局部变量只会进行一次初始化
-
-
说明符和限定符
-
cv限定符:const、volatile(不稳定的)
-
存储说明符:auto、register、static、extern、thread_local、mutable
volatile表明:即使程序代码没有对内存单元进行修改,其值也可能发生变化(如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息,这种情况下硬件可能修改其中的内容)
mutable指出:即使结构(或类)变量为const,其某个成员也可以被修改
struct data{ char name[30]; mutable int accesses; }; const data veep = {"asd", 0}; strcpy(veep.name, "qwe"); // 不允许 veep.accesses++; // 允许
const特性:const全局变量和使用了static说明符是一样的,会限定为内部链接;但如果希望链接性为外部,可以用extern来覆盖:
extern const int states = 50;
单个const在多文件内共享,因此只有一个文件可对其进行初始化
-
-
函数和链接性
所有函数存储持续性都自动为静态,链接性默认为外部
可以使用extern来指出函数是在另一个文件中定义的;用static将函数的链接性设置为内部,必须在原型和定义中均使用改关键字**(静态函数覆盖外部定义)**
内联函数不受这种限制,可以在多个文件中定义,但是定义必须相同
-
语言链接性
C语言中,一个名称对应一个函数;而C++中函数有重载,通常用名称修饰
如果要用C库中预编译的函数,可以用函数原型来指出:
extern "C" void spiff(int);
-
存储方案和动态分配
动态内存由new和delete控制
如果要初始化常规结构或者数组,可以使用大括号
struct point{ int x; int y; }; // C++11特性 point* one = new point{1, 2}; int* arr = new int[4]{1, 2, 3, 4};
new失败时,将引发异常
std::bal_alloc
// 运算符new和new[]分别调用如下函数 void* operator new(std::size_t); // new void* operator new[](std::size_t); // new[] // delete和delete[]同理 void* operator delete(std::size_t); // delete void* operator delete[](std::size_t); // delete[]
侯捷的课
new:先分配memory,再调用构造函数
Complex* pc = new Complex(1, 2); // 编译器大多数会像下面这样转化 Complex* pc; // 是一个函数,分配内存,其内部调用malloc(n) void* mem = operator new(sizeof(Complex)); // 转型(void*转型为我们需要的) pc = static_cast<Complex*>(mem); // 构造函数 pc->Complex::Complex(1, 2);
delete:先调用析构函数,再释放memory
delete ps; // 析构函数 String::~String(ps); // 释放内存,内部调用free(ps) operator delete(ps);
定位new运算符
指定要使用的位置。可以通过这种特性设置其内存管理规程、处理需要通过特定地址进行访问的硬件、在特定位置创建对象
#include<new> struct chaff{ char dross[20]; int slag; }; char buffer1[50]; // *之前的知识点,cout的时候要转换为别的指针,用char会直接 char buffer2[100]; // *输出字符串来 p1 = new (buffer1) chaff; // 从buffer1中分配空间给chaff p2 = new (buffer2) int[20]; // 从buffer2中分配空间给一个包含20个元素的数组
注意:定位new出的空间不能delete,因为不在堆中
定位new运算符的另一种用法时,将其与初始化结合使用,从而将信息放在特定的硬件地址处
-
-
名称空间
// 在名称空间Jack中加入以下变量(若没有则创建) namespace Jack{ double pail; int pal; struct Well {...}; }
名称空间不能位于代码块中
用
::pail
调用全局的pail而不是Jack中的在using namespace(using声明)中,可以有隐藏变量,局部变量隐藏空间变量;而using Jack::pal(using编译指令)中,无法隐藏变量
名称空间可以嵌套
namespace elements{ namespace fire{ int flame; } float water; } // 还可以起别名 namespace EF = elements::fire; // 未命名名称空间,相当于static全局变量(单文件内可用) namespace { int ice; int handycoot; }
名称空间指导原则
- 使用已命名的名称空间中声明的变量,而不是外部全局变量
- 使用已命名的名称空间中声明的变量,而不是静态全局变量
- 如果开发了一个函数库或类库,将其放在一个名称空间中
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计
- 不要在头文件中使用using编译指令
- 导入名称时,首选使用作用域解析运算符或using声明的方法
- 对于using声明,首选将其作用域设置为局部而不是全局