C++11 的初始化方式:列表初始化
列表初始化的方式常用于数组和结构,在 C++98 中也可用于单值变量
int a = {24}; //C++98
int b{7}; //C++11
int c = {}; //C++11,变量将被初始化为0
int d{};
C++11 新增类型:char16_t 和 char32_t
两种类型分别长16位和32位,都是无符号的
在 C++11 中可以使用 u 和 U 两个前缀分别表示两种类型的常量
char16_t ch1 = u'q';
char32_t ch2 = U'\U0000222B';
C++11 中的类型转换
列表初始化时进行的转换对类型转换的要求更加严格,在列表初始化中不允许 缩窄 ,即变量的类型可能无法表示赋给它的值
C++11 中的 auto 声明
在初始化声明中,使用关键字 auto 而不指定变量的类型,编译器将把变量的类型设置成与初始值相同
自动类型推断只允许用于单值初始化
C++11 的数组初始化方法:列表初始化
C++11 的字符串初始化
C++11 允许将列表初始化用于 C-风格的字符串和 string对象
C++11 允许将列表初始化用于结构体(不允许缩窄转换)
基于范围的for循环
int a[5] = {1,2,3,4,5};
for (int temp : a) {
cout << temp;
}
右值引用
double && ref = std::sqrt(36.00);
关键字decltype和后置返回类型
template <class T1, class T2>
void ft(T1 x, T2 y) {
...
decltype(x + y) xpy = x + y;
...
}
//在C++98中,由于类型之间复杂的关系导致无法声明 xpy = x + y 这样的语句
//在C++11中使用关键字 decltype 可以使得变量 xpy 的类型被确定(编译器将遍历核对表)
//必须在声明参数后使用 decltype
//此外,对于类型模板中类型不确定的返回值,C++新增了一种声明和定义函数的语法
template <class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x + y) //后置返回类型,函数原返回值位置的auto作为占位符
{
...
return x + y;
}
线程存储持续性【并行编程】
使用关键字 thread_local 声明的变量的生命周期和所属的线程一样长
使用关键字 register 声明的寄存器变量
在C++11前,可以使用该关键字声明变量,旨在提高变量的访问速度
在C++11后,仅可用于显式地指出变量是自动变量
关键字 constexpr
C++11中可以将列表初始化用于类对象的初始化
只要提供与某个构造函数的参数列表匹配的内容即可
Stock temp = {"It is ok", 100, 10.0};
作用域内枚举
//将枚举的作用域声明为类避免同名冲突
enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Jumbo};
作用域内枚举不能隐式转换为整型
C++11中作用域内枚举的底层类型为 int
C++11中使用头文件 radom 提供了更强大的随机数支持
C++11中可以用 explicit 关键字声明转换运算符函数
explicit operator int() const;
这样声明之后,需要强制类型转换时才调用这些运算符
C++11空指针
在C++98中,字面值0也可以表示空指针,有时候使用 (void*)0 表示,也常通过宏 NULL 表示
在C++11中引入了关键字 nullptr 用于表示空指针
C++11的类内初始化
C++11中允许使用更直观的方式进行初始化(与在构造函数中使用成员初始化列表等价)
class Classy {
int mem1 = 10;
const int mem2 = 20;
...
}
Classy::Classy(int n) : mem1(n) {} //mem2仍被设置为20
C++11中【继承构造函数】的机制
C++11中的模板别名
可以通过 typedef 为模板具体化指定别名,也可以通过C++11提供的模板别名功能:
template <typename T>
using arrtype = std::array<T, 12>;
//使用模板别名
arraytype<int> days;
//用于非模板(与常规typedef等价)
typedef const char* pc1;
using pc2 = const char*;
如定义语句
int carrots
声明了需要的内存以及内存单元的名称
C++不允许将函数定义嵌套在另一个函数定义中
C++中包含如下6种语句:
C++变量名中,以两个下划线或大写字母开头的名称被保留给实现(编译器及其使用的资源使用),以一个下划线开头的名称被保留给实现,用作全局标识符。使用这样的名称会导致编译器行为的不确定性
头文件 climits 中包含了关于整型限制的信息,如 INT_MAX 为 int 类型的最大值
signed char 和 unsigned char
/*
** unsigned char 通常表示的范围为0~255
** signed char 表示的范围为-128~127
*/
宽字符类型 wchar_t
可以表示系统使用的最大扩展字符集,大小取决于底层实现
const 声明常量时若没有提供值,则该常量的值将不确定且无法修改
【类型转换】
初始化和赋值时进行的转换
转换 | 潜在的问题 |
---|---|
较大的浮点类型转换为较小的浮点类型 | 精度降低,值可能超出目标类型的取值范围,这种情况下结果将不确定 |
将浮点类型转换为整型 | 小数部分丢失,原来的值可能超出目标类型的取值范围,这种情况下结果将不确定 |
将较大的整型转换为较小的整型 | 原来的值可能超出目标类型的取值范围,通常只复制右边的字节 |
表达式中的转换
在表达式中,编译器通过校验表确定算数表达式中执行的转换
传递参数时的转换
传递参数时的类型转换通常由 C++ 函数原型控制(可以取消)。取消这种控制时, C++ 将对相应类型应用 提升 ( 把char、unsigned char、short、unsigned short转换成int类型),此外, float 类型将被提升为 double 类型
强制类型转换
/*强制类型转换的通用格式为:*/
(typeName) value;
typeName (value);
此外, C++ 还引入了四个强制转换运算符
拼接字符串常量
任何两个由空白(空格、制表符、换行符)分隔的字符串常量都将自动拼接成一个
也可以使用加号拼接
结构中的位字段
C++ 允许指定占用特定位数的结构成员
struct torgle_register {
unsigned int SN : 4; //冒号后的数字指定了使用的位数
bool goodIn : 1;
}
枚举
枚举量是整型,可以被提升为 int 类型,但 int 类型不能自动转换为枚举类型
enum spectrum{red,blue};
spectrum band;
int color = blue; //valid
band = 3; //invalid
color = 3 + red; //valid
可以使用赋值运算符显示指定枚举量的值
enum bits{one = 1, two = 2, four = 4};
enum bigstep{first, second = 200, third}; //0,200,201
enum {zero, null = 0, one, numero_uno = 1}; //0,0,1,1
枚举的取值范围
enum bits{one = 1, two = 2, four = 4, eight = 8};
bits myflag;
myflag = bits{6}; //valid
指针和自由存储空间
关于指针的初始化
在C++中创建指针时,计算机将独立分配用于存储指针的地址,但不会分配用于存储指针所指向的数据的地址。这可能引发很大的错误,因此一定要在对指针应用解除引用运算符前,将之初始化为一个确定且适当的地址
指针和数字
希望将数字作为地址使用时,应先通过强制类型转换将数字转换为适当的地址类型
运行时内存分配与释放
//malloc()函数
//new运算符:找到一个长度适当的内存块,并返回内存块的地址
//free()函数
//delete运算符
地址本身只指向对象存储地址的开始,没有提供类型或长度信息。其指向的数据的类型是在声明后由编译器保证的
new 运算符和 delete 运算符应当成对使用,否则可能造成【内存泄漏】
也 不应尝试释放已经释放的内存块 ,其结果是不确定的。此外,不能用 delete 释放声明变量获得的内存,应当注意只能使用 delete 释放 new 分配的内存(对空指针使用 delete 是安全的)
内存分配与数组创建
静态联编:通过声明创建数组,在编译时为数组分配内存
动态联编:在运行时根据需要选定长度创建数组
可以通过 new 运算符实现动态联编, new 将返回动态数组的首元素地址
int *p = new int [10];
delete [] p; //使用new[]分配的内存,应使用 delete[]释放
数组的地址
short tell[10];
short *p = &tell[0]; //单独使用tell也被解释为此
short (*q)[20] = &tell; //对tell取地址,得到的是整个数组的地址,即指向一个包含20个short类型元素的数组
对指针变量加一时,其增加的值等同于指针对应类型的字节数
指针和字符串
对于 cout 而言,若指向的指针类型为 char* ,则 cout 将显示指针指向的字符串
C++【内存分配】
自动存储
在函数内部定义的变量使用自动存储空间,称为自动变量
自动变量是局部变量,作用域为其所在的代码块
自动变量通常存储在栈中
静态存储
在函数外定义的变量以及用 static 关键字定义的变量使用静态存储方式,在整个程序执行期间都存在
动态存储
new 和 delete 管理了一个内存池,在C++中称为自由存储空间/堆,该内存池与用于静态变量和动态变量的内存是分开的。使用动态存储的数据生命周期不受程序和函数的生存时间控制
对一组字符串的两种声明方式
//将指针数组声明为一组字符串常量
const int Cities = 5;
const char* cities[Cities] =
{
"sadsadasd",
"asdasdsadsa",
"sadsadd",
"dsdfsdfsdfsf",
"sadsadas"
}
//使用二维数组
const int Cities = 5;
const int maxn = 100;
char cities[Cities][maxn] =
{
"sadsadasd",
"asdasdsadsa",
"sadsadd",
"dsdfsdfsdfsf",
"sadsadas"
}
从存储空间的角度而言,使用指针数组将更为经济
函数原型
括号为空和使用关键字 void 等效,意味着函数没有参数
若只是希望暂时不指定参数,应使用省略号
void my_function(...);
原型中的变量名不必与定义中相同,且可以省略,仅保留类型
void n_char(char, int);
函数原型的使用可以确保:
指针和const
//指向常量的指针
int a = 1;
const int b = 2;
const int *p = &a; //可以直接对a做修改,但不能通过指针修改a
const int *q = &b; //不可以直接对b做修改,也不能通过指针修改b
int *o = &b; //invalid,C++禁止将const的地址赋给非const指针
//指针常量
int * const finger = &a; //finger只能指向a
//指向常量的指针常量
const int * const stick = &b; //stick只能指向b,且不能间接修改b的值
//一级间接关系中,可以将非const指针赋给const指针
int a = 1;
int *p = &a;
const int *q = p;
//二级间接关系中,允许这样做可能使得const数据被修改,这是不安全的,因此编译器将禁止
const int months[12] = {...};
//禁止将常量的地址赋给非常量指针意味着不能将上面的数组名作为参数传递给使用非常量形参的函数
将指针作为函数参数传递时,可以使用指向常量的指针保护数据
int *a[4]; //4个指向int数据的指针组成的数据
int (*a)[4]; //指向4个int数据组成的数组的指针
a[r][c] = *(*(a + r) + c);
函数指针
函数的地址是存储其机器代码的内存的开始地址
使用函数指针
//在C++中,可以通过函数的函数名获得函数地址
double pam(int); //函数原型
double (*p)(int); //可以指向函数pam(返回double类型,有一个int参数的函数)的函数指针
double *q(int); //可以指向一个返回double类型的指针,有一个int参数的函数
函数指针可以看做函数名,在函数调用时使用
double pam(int);
double (*p)(int);
p = pam;
double x = pam(4);
double y = (*p)(4);
double z = p(4);
//对于函数指针p,使用上面两种形式进行函数调用在C++中都是正确的
深入函数指针
//函数原型中可以省略标识符,下列函数特征标含义都相同
const double * f1(const double ar[], int n);
const double * f2(const double *ar, int n);
const double * f3(const double [], int);
const double * f4(const double *, int);
内联函数
在通常的程序执行过程中,当执行至常规函数调用的相关指令时,会跳转到函数的地址以执行函数代码,并在函数执行结束后返回主程序,这个过程需要一定的开销
若使用内联函数,则编译器将使用相应的函数代码替换函数调用
内联函数运行速度比常规函数稍快,代价是占用更多内存
//使用关键字 inline 声明内联函数
inline double square(double x) {return x * x};
当函数过大或函数存在递归调用时,将函数声明为内联函数会被禁止
此外,宏定义是内联代码的原始实现,但宏不能按值传递
#define SQUARE(X) ((X)*(X))
inline double square(double x) {return x * x};
int main() {
double a = 5.0;
double b = SQUARE(a++); //a将递增两次
double c = square(a++); //a将递增两次
}
引用变量
作为变量
int a;
int & b = a; //b将作为变量a的别名
引用必须在声明时进行初始化,且初始化后不可以再被改变
作为函数参数
函数的引用参数将会被初始化为函数调用传递的实参
若希望使用引用,但不希望进行修改,则可以使用常量引用
int swap(const int & a, const int & b);
当参数为const引用时,C++将在实参与引用参数不匹配时生成临时变量
//1. 实参类型正确,但不是左值(可通过地址访问的数据对象)
//2. 实参类型不正确,但可以转换为正确的类型
double refcube(const double &ra) {
return ra * ra * ra;
}
double side = 3.0;
long edge = 5L;
double c1 = refcube(side + 10.0);
double c2 = refcube(edge);
double c3 = refcube(7.0);
// 对于c1和c3的调用,编译器将生成一个临时匿名变量,并让ra指向它
// 对于c2的调用,double引用无法指向long
//此外,string类定义了一种从 char* 到string的转换功能,因此可以使用C-风格的字符串初始化string。体现在函数参数中时:
string version(const string & s1, const string & s2) {
string temp;
temp = s1 + s2;
return temp;
}
//可以将实参char*或const char*传递给形参const string &,函数将创建一个正确的临时变量,使用转换后的实参将之初始化,然后传递指向该临时变量的引用
值得一提的是,应当避免函数终止时返回不存在的内存单元的引用
默认参数
可以为函数参数指定一个值,当函数调用省略实参时将自动使用这个值
int harpo(int n, int m = 4, int j = 5);
对于带参数列表的函数,必须从右向左添加默认值
如上的harpo()原型允许调用时提供一个,两个或三个参数。实参按照从左向右的顺序被赋给相应形参,而不能跳过任何参数
函数重载
C++中允许定义特征标不同的同名函数(函数特征标,即函数的参数列表),编译器将对每一个重载函数进行 名称修饰
使用被重载的函数时,需要在函数调用里使用正确的参数类型。若调用时提供的参数与多个原型匹配,调用将被拒绝
//类型引用和类型本身将被视为同一个特征标
double cube(double x);
double cube(double & x);
void dribble(char * bits);
void dribble(const char * bits);
void dabble(char * bits);
void drivel(const char * bits);
//对于dribble函数,编译器将根据实参是否带有const选择相应的原型
//对于dabble函数,仅可以与带非const参数的调用匹配
//对于drivel函数,const和非const的参数都可以匹配
可以以不同特征标以及不同返回值的形式重载函数(但不允许返回值不同,特征标相同的函数重载)
此外,还需要不同引用类型的重载
//与可修改的左值参数匹配
void sink(double & rs);
//与可修改的左值参数、const左值参数以及右值参数匹配
void sink(const double & r2);
//与右值参数匹配
void sink(double && r3);
【左值与右值】
函数模板
函数模板允许以任意类型的方式定义函数(使用关键字 class 或 typename)
template <typename AnyType> //指出要建立一个模板,并给出一个类型名
void swap(AnyType &a, AnyType &b) { //并非所有的模板参数都必须是模板参数类型
...
}
模板不创建任何函数,只是告知编译器如何定义函数。当需要使用某个具体类型时,编译器将按照模板创建对应的函数
但模板存在一定的局限性,并非所有的类型都使用相同的算法。可以考虑如下的解决方案:
重载模板
template <typename T>
void Swap(T &a, T &b) {
...
}
template <typename T>
void Swap(T *a, T *b, int n) {
...
}
为特定类型提供具体化的模板
/*
** C++98的具体化方法:
** 1. 对于给定的函数名,可以有非模板函数,模板函数和显示具体化模板函数,以及他们的重载版本
** 2. 显示具体化的原型和定义以 template<> 开头,并通过名称指出类型
** 3. 具体化优先于常规模板,非模板函数优先于具体化和常规模板
*/
struct job {
...
};
void Swap(job &a, job &b);
template <typename T>
void Swap(T &a, T &b);
template <> void Swap<job>(job &a, job &b);
//等价声明template <> void Swap(job &a, job &b);
实例化和具体化
在代码中包含函数模板本身不会生成函数定义
template <typename T>
void Swap(T &a, T &b) {
...
}
//模板的显示实例化
template void Swap<double>(double, double); //编译器将生成Swap()的一个对于double类型的实例
int main() {
int i = 1, j = 2;
//模板的隐式实例化
Swap(i, j); //对于两个int类型的函数调用导致编译器生成Swap()的一个对于int类型的实例
}
隐式实例化、显式实例化和显式具体化统称为具体化
在同一个文件或转换单元中使用同一种类型的显式实例化和显式具体化将出错
编译器选择使用哪个函数版本——【重载解析】
/*
** 1. 创建候选函数列表,其中包含同名函数和同名模板函数
** 2. 使用候选函数列表创建可行函数列表,为此有一个隐式转换序列
** 3. 确定是否有最佳可行的函数,否则该函数调用出错
*/
/*
** 从可行函数中确定最佳可行函数,优先顺序大致如下
** 1. 完全匹配,但常规函数优先于模板函数(完全匹配中允许一些无关紧要的转换)
** 2. 提升转换(float->double)
** 3. 标准转换(int->char)
** 4. 用户定义的转换(如类声明中定义的转换)
*/
//指向非const数据的指针和引用优先与非const指针和引用参数匹配
//模板函数的匹配将根据部分排序规则找出最[具体]的模板
程序文件的单独编译
C++程序可以拆分为独立的文件,单独对它们编译后链接为可执行程序。可以将文件分为三部分:
头文件(.h):包含结构声明和使用这些结构的函数的原型,可以通过include指令包含头文件
源代码文件(.cpp):包含结构相关函数的代码
源代码文件(.cpp):包含调用结构相关函数的代码
在源代码文件中包含头文件可以通过如下方式:
#include //编译器在存储标准头文件的文件系统中查找
#include"myProject" //编译器优先在当前工作目录查找
在头文件中可以使用预处理器编译指令避免被多次包含:
//关于头文件中的编译指令:#ifndef和#pragma once
#ifndef _COMPLEX_
#define _COMPLEX_
//...
#endif
#pragma once
//...
//两种编译指令都可以防止头文件被包含多次
//#ifndef方式受C/C++语言支持,也可以保证内容完全相同的两个文件不被重复包含,但可能因为宏定义造成找不到声明的问题,在大型项目中也会增加编译时间
//#pragma once方式由编译器保证,声明只作用于文件,但遇到如头文件多份拷贝的情况不能保证他们不被重复包含
//#pragma once不跨平台,但#ifndef不受编译器限制
存储持续性:描述数据的生命周期
作用域:描述名称在文件中的可见范围
链接性:描述名称如何在不同单元中共享
存储说明符:
cv-限定符:
const:指出内存被初始化后,程序不能再对其进行修改
const声明的外部静态变量的链接性为内部的(可以使用extern关键字覆盖默认的内部链接性)
volatile:指出即使程序没有进行修改,内存单元中的值也可能发生变化(可能来自硬件或其他程序影响)
编译器可能在某些变量的多次使用中进行优化,将其值存储到寄存器中使用而不是多次查找。使用关键字 volatile 声明将会阻止这样的优化
生命周期:在函数定义外使用的变量以及使用关键字 static 定义的变量为静态存储持续性,在整个程序的运行过程中都存在
链接性:静态存储持续性的变量可以有三种链接性:
外部链接性:声明在函数外
对外部链接性的静态变量的使用需要遵循单定义规则,即仅允许有一次定义声明,但可以有多次引用声明。在某个文件中定义声明过的外部静态变量可以在其他文件中通过关键字 extern 进行引用声明后使用
在局部语句块遇到同名的自动变量时,直接使用变量名将视为使用局部的自动变量,但可以通过作用域解析运算符解决这个问题
//external.cpp
double warming = 0.3;
//support.cpp
extern double warming;
void local_function() {
double warming = 0.8; //hides external variable
cout << warming << endl; //use global variable
cout << ::warming << endl; //use external variable
}
内部链接性:声明在函数外,使用 static 修饰
文件中的静态内部变量将会隐藏同名的静态外部变量
无链接性:声明在函数内,使用 static 修饰
静态局部变量只在启动时进行一次初始化, 当代码块处于不活动状态时仍然存在 ——在两次函数调用之间,静态局部变量的值将保持不变
作用域:取决于声明的位置,可以是局部或整个文件
存储方式:编译器将分配 固定内存 存储所有静态变量
初始化:可以对静态变量进行默认的零初始化,也可以进行常量表达式初始化和动态初始化
#include
int x;
int y = 1;
int z = 2 * 2;
const double pi = 4.0 * atan(1.0);
对于如上的静态变量,首先都会被零初始化
若有常量表达式初始化了变量,编译器将执行常量表达式的初始化
若没有足够的信息,变量将等待在后续的链接执行中动态初始化
生命周期:使用 new 运算符或 malloc() 函数分配的内存为动态内存,被分配内存的变量为动态存储持续性
链接性:不受链接性规则控制,但适用于跟踪动态内存的自动或静态指针变量
作用域:不受作用域规则控制,但适用于跟踪动态内存的自动或静态指针变量
存储方式:动态变量被管理在堆内存中,分配和释放顺序取决于 new 和 delete 的使用( new 运算符请求的大块内存可能不会在程序结束时自动释放,应当注意使用 delete 释放这部分内存)
初始化:
使用 new 运算初始化
//C++98
int * p = new int(1);
//C++11 初始化常规结构或数组
struct point {
int x, y, z;
}
int *ar = new int[4] {1, 2, 3, 4}; //用于数组
point *p = new point {1, 2, 3}; //用于结构体
int *q = new int {3}; //用于单值变量
初始化失败时,早期C++中返回空指针,后来引发异常 std::bad_alloc
相关函数
//运算符new和new[]将分别调用如下的分配函数
void * operator new(std::size_t);
void * operator new[](std::size_t);
//运算符delete和delete[]将分别调用如下释放函数
void operator delete(void *);
void operator delete[](void *);
//eg
int *p = new int; //将被转换为int *p = new(sizeof(int));
在C++中,这些内存分配和释放函数是可替换的,即可以对其进行定制重载以满足内存分配的需求
定位 new 运算符
定位 new 运算符的分配函数接收两个参数,请求的字节数和指定的内存地址,在指定的内存单元尝试分配请求的空间。但其不保证指定的内存单元是否已经被使用
#include
struct chaff {
char dross[20];
int slag;
}
char buffer1[50];
char buffer2[200];
int main() {
chaff *p1, *p2;
int *p3, *p4;
p2 = new (buffer1) chaff; //从buffer1中分配空间给结构chaff
p4 = new (buffer2) int[20]; //...
}
所有函数自动为静态存储持续性
函数的默认链接性是外部的,可以使用 static 关键字修饰定义和原型,使链接性为内部(将覆盖外部同名函数的定义)
内联函数不受单定义规则制约,但同名内联函数的所有定义必须都相同
extern "C" void spiff(int); //使用C语言链接性,内部翻译为_spiff
exyern "C++" void spaff(int); //使用C++语言链接性,内部进行名称修饰
C++中引入了名称空间以避免同名变量之间的冲突
namespace myspace {
int a;
void fetch();
}
...
namespace myspace { //再次使用名称空间添加新内容
double b;
void fetch() {
...
}
}
名称空间可以是全局的,也可以嵌套在另一个名称空间中,但不能位于代码块中。默认情况下其中的变量的链接性为外部
可以将名称加入到已有的名称空间中(再次使用名称空间以添加新的变量名称或补充已有函数的定义)
访问名称空间中的名称时,应通过作用域解析运算符
cout << myspace::a << endl;
myspace::fetch();
using声明可以将名称空间中的特定名称添加到其所属的声明区域中
namespace myspace {
int a;//myspace
}
int a;//global
int main() {
using myspace::a;
int a; //error
cin >> a; //myspace
cin >> ::a; //global
}
在函数外使用using声明时,将把名称添加到全局名称空间中
using编译指令将使名称空间中的所有名称都可用
#include
using namespace std; //使名称空间std中的内容全局可用
假设名称空间和声明区域定义了相同的名称,使用using声明会发生冲突,而使用using编译指令导入的名称会被局部版本隐藏
名称空间的嵌套
namespace myth {
...
using namespace elements;
namespace sadsa {
namespace sadsad {
...
}
}
}
---
using namespace myth; //using编译指令可传递,命名空间elements也会被导入
namespace MS = myth::sadsa; //可以设置名称空间别名以简化对嵌套名称空间的使用
using namespace MS::sadsad;
匿名名称空间
namespace
{
int ice;
int bandycoot;
}
匿名名称空间的潜在作用域为声明位置到声明区域末尾
类似于链接性为内部的静态变量
类是一种将抽象转化为用户自定义类型的C++工具,它将数据表示和操纵数据的方法组合为一个整洁的包
一般来说,类规范由两个部分组成:
C++中使用访问控制关键字描述对类中成员的访问权限,以实现数据隐藏
class className
{
... //默认的访问控制为private
private: //私有部分
...
public: //公有部分
...
protected: //用于继承的关键字
...
};
使用类对象的程序可以直接访问公有部分,但只能通过公有成员函数或友元函数访问私有成员
对于类实现,通常会提供两个代码文件:
存放类定义代码的头文件
#pragma once
//stock00.h
#include
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
void acquire(const std::string& co, long n, double pr);
...
};
存放类实现代码的源代码文件
#include "stock00.h"
#include
//stock00.cpp
//使用作用域解析运算符指出成员函数所属的类
void Stock::acquire(const std::string& co, long n, double pr) {
company = co;
if (n < 0) {
std::cout << "Number of shares can't be negative;"
<< company << "shares set to 0.\n";
shares = 0;
}
else {
shares = n;
}
share_val = pr;
set_tot();
}
...
定义位于类声明中的函数将自动成为内联函数,如Stock类中的 set_tot() 方法。也可以在类声明之外定义成员函数并使用 inline 关键字使其成为内联函数
内联函数要求在每个使用它的文件中都对它进行定义,可以将内联定义放在定义类的头文件中以确保内联定义对多文件程序可用
创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员,但同一个类的所有对象共享同一组类方法(每个方法只有一个副本)
C++中通过提供类构造函数以实现类对象的初始化
声明和定义构造函数
构造函数的名称与类名相同:
class Stock
{
private:
...
public:
...
//类构造函数声明
Stock(const std::string& co, long n, double pr);
};
//类构造函数定义
Stock::Stock(const std::string & co, long n, double pr) {
company = co;
if (n < 0) {
std::cout << "Number of shares can't be negative;"
<< company << "shares set to 0.\n";
shares = 0;
}
else {
shares = n;
share_val = pr;
set_tot();
}
}
值得一提的是,构造函数的参数名不能与类成员相同
使用构造函数
//显式调用构造函数
Stock food = Stock("World Cabbage", 250, 1.25);
//隐式调用构造函数
Stock garment("Furry Mason", 50, 19.0);
默认构造函数
若没有定义任何构造函数时,编译器会提供默认构造函数。若提供了非默认构造函数,则必须显式地提供默认构造函数
class Stock
{
private:
...
public:
...
//类构造函数声明
Stock(const std::string& co, long n, double pr);
//默认构造函数声明
Stock();
};
//默认构造函数定义
Stock::Stock() {
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
//使用默认构造函数的情况
Stock first;
Stock second = Stock();
Stock *third = new Stock;
当对象过期时,程序将自动调用类析构函数来完成清理工作。默认情况下,编译器会生成隐式析构函数
对析构函数的调用时机由编译器决定,对于自动变量、静态变量和动态变量有不同的调用时机
通常不应当显式地调用析构函数
//构造函数的声明
class Stock
{
private:
...
public:
...
~Stock();
...
};
//构造函数的定义
Stock::~Stock() {
//TODO std::cout << "Bye, " << company << "!\n";
}
默认情况下,将一个对象赋给另一个同类型对象时,C++将源对象的每个数据成员的内容复制到目标对象的相应数据成员中
值得一提的是,可以通过构造函数创建一个新的临时对象以进行赋值操作(临时对象会在赋值结束后调用析构函数被删除)
stock1 = Stock("Nifty Foods", 10, 50.0); //总会创建临时对象
Stock stock2 = Stock("Boffo Objects", 2, 2.0); //可能会创建临时对象
接收一个参数的构造函数允许使用赋值语法将对象初始化为一个值
可以以后置 const 的形式声明成员函数,以保证函数不会修改成员数据
//const成员函数
void show() const;
void Stock()::show() const {
...
}
this指针
在类函数中可以使用this指针,this指针指向调用成员函数的对象
对象数组
初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应元素中。因此创建类对象数组的类必须有默认构造函数
Stock stocks[4] = {
Stock("NanoSmart", 12.5, 20),
Stock("Boffo Objects", 200, 2.0),
Stock("Monolithic Obelisks", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5)
}
作用域为类的常量
使用 static 关键字
class Bakery {
private:
static const int Months = 12;
double costs[Months];
}
枚举
class Bakery {
private:
enum {Months = 12};
double costs[Months];
}
C++中允许将运算符重载扩展到用户定义的结构体和类,这个过程通过运算符函数实现。当编译器发现操作数是重载了运算符的类对象时,就会使用相应的运算符函数替换运算符
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
...
Time operator+(const Time& t) const; //重载加法运算符
...
};
//实现
Time Time::operator+(const Time & t) const {
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
//使用
Time total(5, 10), coding(2, 30), fixing(7, 55);
total = coding.operator+(fixing);
total = coding + fixing;
Time t1, t2, t3, t4;
t4 = t1 + t2 + t3; //t4 = t1.operator+(t2.operator+(t3));
在运算符表示法中,运算符左侧的对象是调用对象,右边的对象是作为参数传递的对象
重载后的运算符必须至少有一个是用户定义的类型(防止用户为标准类型重载运算符)
使用运算符不能违反运算符原来的句法规则
不能创建新运算符
不能重载一部分特殊运算符
sizeof运算符: sizeof
成员运算符 (className).
成员指针运算符 .*
作用域解析运算符 ::
条件运算符 ?:
强制类型转换运算符 xxx_cast
一个RTTI运算符 typeid
一部分运算符只能通过成员函数进行重载
=
()
[]
->
有一类特殊的非成员函数可以直接访问类的私有成员,即友元函数
创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend
friend Time operator*(double m, const Time& t);
友元函数虽然在类声明中声明,但并不属于成员函数
//Time类重载乘法运算符的友元函数的两种实现方式
friend Time operator*(double m, const Time& t) {
return t * m;
}
Time operator*(double m, const Time& t) {
Time result;
long totalminutes = t.hours * m * 60 + t.minutes * m;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
//Time重载<<运算符以输出显示
//(参数和返回值中ostream都为引用,这样可以满足连续输出的需要)
std::ostream& operator<<(std::ostream& os, const Time& t) {
os << t.hours << "hours, " << t.minutes << "minutes";
return os;
}
通过只有一个参数或只有一个参数不是默认参数的构造函数可以实现对于该参数数据类型的 隐式转换
Stonewt(double lbs);
...
Stonewt myCat;
myCat = 19.6;
其实质过程是将 构造函数作为了自动类型转换函数 ,在赋值的过程中以该参数构造临时对象以完成赋值
隐式转换包括:
可以通过关键字 explicit 关闭这项特性,但仍允许 显式转换
explicit Stonewt(double lbs);
...
Stonewt myCat;
myCat = 19.6; //invaild
myCat = (Stonewt)19.6 //valid
C++中提供了一种运算符函数用于类变量到基本类型的转换过程,其基本形式如下
operator typeName();
// 转换函数必须是类方法
// 转换函数不能指定返回值
// 转换函数不能有参数
过多的转换函数可能导致二义性,应当谨慎地使用隐式转换函数
无论创建了多少对象,程序都只创建一个静态类变量副本
不能在类声明中初始化静态成员变量,除非静态数据成员为const整数类型或枚举型
字符串单独保存在堆内存中,对象仅保存了指出到哪里查找字符串的信息
删除对象可以释放对象本身占用的内存,但不能自动释放属于对象成员的指针指向的内存,因此在类中必须注意释放动态内存
在创建类时,C++将自动提供以下成员函数:
默认构造函数,若没有定义构造函数
在没有提供任何构造函数时,C++将为类创建默认构造函数——该默认构造函数不接受任何参数,也不进行任何操作,使用默认构造函数创建的对象的值是未知的
如果定义了构造函数,则默认构造函数不会被提供,需要自行提供才可以在创建对象时不显式地进行初始化
带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值
只能存在一个默认构造函数
默认析构函数,若没有定义析构函数
复制构造函数,如果没有定义复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中,类的赋值构造函数原型通常如下:
Class_name(const Class_name &);
新建一个对象并将其初始化为同类现有对象时,程序生成了对象副本时(函数值传递和返回对象)复制构造函数都将被调用
默认复制构造函数将逐个复制非静态成员【浅拷贝】
若类中包含了使用 new 运算符初始化的指针成员,应当定义一个复制构造函数进行【深拷贝】,复制成员的同时也复制指针所指向的数据
赋值运算符,如果没有定义
C++允许类对象赋值,这是通过自动为类重载运算符实现的,类的默认赋值运算符重载通常如下:
Class_name & Class_name::operator=(const Class_name &);
将已有对象赋给另一个对象时,将会使用重载的赋值运算符
使用默认的赋值运算符可能导致一些问题,因此必要时应当主动提供赋值运算符定义,但应当注意:
地址运算符,如果没有定义
关于【初始化】
StringBad headline1("cELERY sTALKS AT mIDNIGHT");
...
StringBad knot;
knot = headline; //使用赋值运算符
...
StringBad metoo = knot; //?使用复制构造函数,或构造临时对象后调用赋值运算符
静态类成员函数
静态类成员函数不能通过对象调用,也不可以使用 this 指针。在公有区域声明的静态类成员函数可以通过类名和作用域解析运算符调用
静态类成员函数只能使用静态数据成员
在类中使用 new 初始化对象的指针成员应当:
返回对象将调用复制构造函数,但返回引用时则不会
总之,若方法或函数要返回局部对象,则应当返回对象,而非指向对象的引用,这种情况下将会使用复制构造函数来生成返回的对象。若方法或函数要返回一个没有公有复制构造函数的类的对象(如ostream),它必须返回一个指向这种对象的引用
在方法或函数可以返回对象也可以返回引用时应当选择引用,这样有更高的效率
使用对象指针时,需要注意:
在使用【定位 new 运算符】在缓冲区中创建对象时,需要显式地调用相关对象的析构函数以销毁它们
?对于使用定位 new 运算符创建的对象,应当以与创建顺序相反的顺序进行删除(晚创建的对象可能依赖于早创建的对象),当所有对象被销毁后,才可以释放存储这些对象的缓冲区
成员初始化列表语法
类构造函数可以使用成员初始化列表语法以初始化数据成员
class Classy {
private:
int mem1;
int mem2;
int mem3;
public:
Classy(int n, int m);
}
//成员初始化列表语法
Classy::Classy(int n, int m):mem1(n), mem2(0), mem3(n*m + 2) {}
应当注意:
【类与const】
使用初始化列表语法和常规定义
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hashTable;
public:
TableTennisPlayer(const string& fn = "none",
const string & ln = "none", bool ht = false);
void Name() const;
bool HashTable() const { return hashTable; };
void ResetTable(bool v) { hashTable = v; };
};
//...
TableTennisPlayer::TableTennisPlayer(const string& fn = "none",
const string& ln = "none",
bool ht = false) : firstname(fn),
lastname(ln), hashTable(ht) {}
TableTennisPlayer::TableTennisPlayer(const string& fn = "none",
const string& ln = "none",
bool ht = false) {
firstname = fn;
lastname = ln;
hashTable = ht;
}
对于使用初始化列表语法的情况而言,它将直接使用string的复制构造函数将 firstname 初始化
而对于第二种情况,将首先为 firstname 调用 string 的默认构造函数,再调用其赋值运算符设置 firstname
class RatedPlayer : public TableTennisPlayer {
...
};
派生类声明头后面的部分表明了基类和派生类别(如上为公有派生)
派生类对象包含基类对象,其存储了基类的数据成员(继承实现),并可以使用基类的方法(继承接口)
派生类需要自己的构造函数,可以根据需要添加额外的数据成员和成员函数
派生类不能直接访问继承自基类的私有成员,必须通过基类方法访问,因此派生类构造函数必须使用基类构造函数
除非要使用默认构造函数,否则应当在派生类构造函数中使用初始化列表语法显式地指定基类构造函数
//使用基类默认构造函数
RatedPlayer::RatedPlayer(unsigned int r, const string& fn,
const string& ln, bool ht) { rating = r; }
//指定基类构造函数并对派生类成员也使用初始化列表语法
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp)
: TableTennisPlayer(tp), rating(r) {}
创建派生类对象时,程序将首先调用基类构造函数,然后再调用派生类构造函数,分别用于初始化继承自基类的数据成员和新增的数据成员。派生类的构造函数总是调用一个基类构造函数——可以通过初始化列表语法指定要使用的基类构造函数,否则编译器将使用基类的默认构造函数
派生类对象过期时,将首先调用派生类析构函数,再调用基类析构函数
派生类可以使用基类的非私有方法
基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显式类型转换的情况下引用派生类对象,但基类指针或引用只能调用基类方法
但不允许将基类对象的指针和地址赋给派生类引用和指针
引用兼容性属性可以将基类对象初始化为派生类对象,也可以将派生类对象赋给基类对象
这两种情况将会把派生类对象作为基类对象的引用参数,调用基类的复制构造函数和重载赋值运算符
多态公有继承,即同一个方法在基类和派生类中的行为不同。实现多态公有继承可以借助两种机制:
第一种机制通常用于对象直接调用方法,将基类和派生类方法行为区分的关键是在派生类中重新定义方法,并实现为不同的行为。如果需要在派生类重新定义的方法中使用基类的同名方法,应当注意使用作用域解析运算符来调用
在基类中使用关键字 virtual 声明的方法将会成为虚方法(在基类中声明为虚方法时,其将在派生类中自动成为虚方法),关键字 virtual 只用于类声明的方法原型中
使用第二种机制通常用于对象的指针或引用。如果一个方法是通过指针或引用调用的,则将会根据指针或引用的类型选择基类或派生类的方法;但通过该方法是一个虚方法,则将会根据指针或引用指向的对象本身的类型选择方法
将源代码中的函数调用解释为执行特定的函数代码块被称为 函数名联编 。在编译过程中进行联编被称为静态联编,在程序运行时选择正确的方法被称为动态联编
强制转换
将派生类的指针或引用转换为基类指针或引用被称为 向上强制转换 ,反之则成为 向下强制转换
向上强制转换允许隐式进行,但向下强制转换则必须显式地进行
对于使用基类引用或指针作为参数的函数调用,将会进行向上转换
隐式的向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,在C++中使用虚成员函数以满足这种需求(编译器对非虚方法使用静态联编),但动态联编会带来额外的开销
通常编译器处理虚函数的方法是:为每个对象添加一个隐藏成员,其中保存了一个指向函数地址数组的指针,其指向的数组被称为 虚函数表
虚函数表中存储了为类对象进行声明的虚函数的地址
派生类对象将包含一个指向虚函数表的指针。若派生类中提供了虚函数的新定义,则该虚函数表将保存新函数的地址,否则该表将保存函数原始版本的地址。此外,派生类定义的新虚函数也将会添加到虚函数表中
调用虚函数时,程序将会查看存储在对象中的虚函数表的地址,然后转向相应的函数地址表,按照对应的顺序使用数组中相应地址指向的函数
使用虚函数在内存和执行速度方面会带来一定的成本:
构造函数不能作为虚函数
析构函数应当是虚函数,除非类不作为基类,即使该虚析构函数不做任何操作
友元函数不能作为虚函数
若派生类没有重新定义函数,将使用该函数的基类版本;若派生类处于派生链中,将会使用最新的虚函数版本,除非基类版本是隐藏的
【重新定义将会隐藏方法】?
重新定义派生类中的函数时,将会使用相同的参数列表覆盖基类声明,无论参数列表是否相同(这将会隐藏所有同名的基类方法)
class Base {
public:
virtual void showperks(int a) const;
}
class Drived : public Base {
public:
virtual void showperks() const;
}
这引出了两条经验规则:
如果重新定义继承的方法,应确保与原来的原型完全相同
但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针
//返回类型协变
class Base {
public:
virtual Base& build(int n);
}
class Drived : public Base {
public:
virtual Drived& build(int n);
}
若基类声明被重载了,则应当在派生类中重新定义所有的基类版本,否则其他版本将被隐藏
纯虚函数
C++中通过纯虚函数提供未实现的函数(纯虚函数结尾处为 = 0):
//一个实现虚函数的范例
class AcctABC {
...
public:
virtual void Withdraw(double amt) = 0;
virtual void ViewAcct() const = 0;
...
}
当类声明中包含纯虚函数时,则指出类是一个抽象基类。此时不能创建该类的对象
C++允许【纯虚函数定义】。抽象类可以对纯虚函数进行定义,在其子类中也可以使用抽象父类的缺省实现
基类使用 new ,派生类不使用 new
对于一个使用了动态内存分配的基类而言,需要为其显式地提供析构函数、复制构造函数以及重载赋值运算符,但对于其没有使用动态内存分配的派生类而言,不需要提供它们
派生类的默认 [析构函数] 会在执行完自身的代码后调用基类的析构函数
派生类的默认 [复制构造函数] 会使用显式的基类复制构造函数以复制派生类中继承自基类的部分
派生类的默认 [赋值运算符] 同样会自动使用基类的赋值运算符以完成对基类组件的赋值
基类使用 new ,派生类也使用 new
当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数和赋值运算符重载必须显式地提供,并且需要使用相应的基类方法处理继承自基类的元素
派生类的 [析构函数] 自动调用基类的构造函数,因此只需要处理派生类中的动态内存分配
派生类的 [复制构造函数] 可以通过初始化成员列表指定基类的复制构造函数来完成
派生类的 [赋值运算符] 则需要通过作用域解析运算符显式地调用基类赋值运算符重载
valarray类
一个擅长于处理数值的模板类,相比 vector 以及 array 提供了更多的算术支持,如可以通过其成员方法返回元素总和和最大值等
double gpa[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
valarray<double> v4(gpa, 4); //使用数组gpa的前四个元素进行初始化
Test类的案例
class TestA {
private:
int a;
public:
TestA() : a(0) {
cout << "调用了A的默认构造函数" << std::endl;
}
TestA(int i) : a(i) {
cout << "调用了TestA的非默认构造函数" << std::endl;
}
TestA(const TestA& A) {
this->a = A.a;
cout << "调用了TestA的复制构造函数" << std::endl;
}
TestA& operator=(const TestA& A) {
this->a = A.a;
cout << "调用了TestA的赋值运算符重载" << std::endl;
return *this;
}
};
class TestB {
private:
TestA A;
public:
TestB() : A() {}
//TestB(int i) : A(i) {}
TestB(int i) : A(i) {}
};
//对于如下的构造函数
TestB(int i) : A(i) {}
TestB(int i) { A = TestA(i); }
//C++要求在构建对象的其他部分之前,先构建继承(包含)对象的所有成员对象,省略初始化列表语法将会导致成员对象所属类的默认构造函数先被调用以初始化成员对象。而使用初始化列表语法则仅会直接调用匹配的构造函数
初始化顺序
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序
使用私有继承,派生类会继承基类实现,基类的公有成员和保护成员都将成为派生类的私有成员
私有继承将基类对象作为一个未命名的继承对象添加到类中
初始化基类组件(通过类名调用构造函数)
//before
Student() : name("Null Student"), scores() {}
//after
Student() : string("Null Student"), valarray<double>() {}
访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法,即在派生类中使用类名和作用域解析运算符调用基类方法
访问基类对象
在私有继承中想要访问内部的基类对象时,需要使用强制类型转换:
...
return (const string&) *this; //将派生类对象强制转换为继承而来的内部的string对象,使用引用是为了避免调用构造函数创建新的对象
...
访问基类的友元函数
试图访问基类的友元函数需要显式地将派生类对象转换为继承而来的内部基类对象,以在相应的情境下调用对应的友元函数:
std::ostream& operator<<(std::ostream& os, Student& stu) {
os << "Scores for" << (const string &) stu << ":\n"; //将会调用string类的友元函数以输出
}
使用包含还是私有继承
通常应当使用包含来建立 has-a 关系。但如果新类需要访问原有类的保护成员,或者需要重新定义虚函数,则应当使用私有继承
保护继承
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员
当从派生类派生出第三代类时,基类的公有成员在第二代类中成为了保护成员,这对于第三代类而言仍是可访问的
公有继承 | 保护继承 | 私有继承 | |
---|---|---|---|
公有成员-> | 公有成员 | 保护成员 | 私有成员 |
保护成员-> | 保护成员 | 保护成员 | 私有成员 |
私有成员-> | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
向上隐式转换 | 可以 | 可以(仅派生类中) | 不可以 |
使用using重新定义访问权限
在保护派生和私有派生中,如果希望让基类的方法在派生类外可用,可以采取两种办法:
定义一个使用该基类方法的派生类方法(访问声明,bass_class::function_name(); )
使用一个using声明指出派生类可以使用特定的基类成员
class Student : private string, private valarray<double> {
...
public:
using valarray<double>::min;
using valarray<double>::max;
...
}
使用上述using声明使得基类成员如同派生类的公有方法一样可用
class Worker {...}
class Waiter : public Worker {...}
class Singer : public Worker {...}
class SingerWaiter : public Waiter, public Singer {...} //派生类中将会间接包含两个Worker子对象
假如尝试使用如下的赋值将会存在二义性:
SingerWaiter ed;
Worker *pw = &ed; //ambiguous
通常这种赋值会将基类指针设置为派生对象中的基类对象的地址,但上面的 ed 中包含两个 Worker 对象,有两个地址可以选择
但可以通过强制类型转换指定对象:
Worker *pw1 = (Waiter*) &ed;
Worker *pw2 = (Singer*) &ed;
虚基类可以使从多个基类相同的类派生出的对象只继承一个基类对象
class Singer : virtual public Worker {...}
class Waiter : virtual public Worker {...}
class SingingWaiter : public Singer, public Waiter {...}
但虚基类带来了新的构造函数规则:
//before
SingerWaiter(const Worker& wk, int p = 0, int q = 0) : Waiter(wk, p), Singer(wk, v) {}
//after
SingerWaiter(const Worker& wk, int p = 0, int q = 0) : Worker(wk), Waiter(wk, p), Singer(wk, v) {}
C++在基类是虚的时候,禁止信息通过中间类自动传递给基类,因此采取以往的方式会导致调用 Worker 的默认构造函数以构建基类组件。而新的规则下需要显式地调用基类构造函数
在多重继承中,假使派生类没有重新定义某一方法,则将使用最近祖先中的定义。但若多个直接祖先中都有该方法的重新定义,则调用将会导致二义性
可以使用作用域解析运算符以解释意图,或在派生类中重新定义该方法以指明想要使用的父类版本
//通过作用域解析运算符指明想要使用的版本
SingerWaiter sw;
sw.Singer::show();
在单继承的派生链中通过递增的方式调用方法是可行的,但这种方式在多重继承中可能无效:
//递增地调用基类方法
class A {
...
public:
void show() const;
}
class B : public A {
...
public:
void show() const {
A::show();
}
}
class C : public B {
...
public:
void show() const {
B::show;
}
}
//多重继承中的情况
class SingingWaiter : public Singer, public Waiter {
...
public:
void show() const {
//一种情况,忽略了Waiter组件
Singer::show();
//另一种情况,继承自Worker的组件将会显示两次
Singer::show();
Waiter::show();
}
}
一个可行的解决方案是采用模块化的方式,使得每个类自己的方法仅负责显示自己的组件,当派生类中需要时将这些方法组合
//一个模板类的声明案例
template<class Type>
class Stack
{
private:
enum {MAX = 10};
Type items[MAX];
int top;
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type& item);
bool pop(Type& item);
};
不能将模板类的函数放在独立的实现文件里(C++11以前提供了关键字 export 支持这一方法)。由于模板不是函数,并不能被单独编译,必须和特定的实例化请求一起使用,因此比较简单的方式是将所有模板信息放在一个头文件中
//案例中构造函数的实现。每一个成员函数的实现都需要单独再进行一次模板声明
template <class Type>
Stack<Type>::Stack() {
top = 0;
}
//必须显式地提供所需的类型,且使用的算法必须与类型一致
Stack<int> kernels;
Stackl<string> colonels;
隐式实例化
在需要时声明一个或多个类对象并指定类型,而编译器会根据模板类生成具体的类定义
在需要对象之前,不会生成类的隐式实例化
ArrayTP<int, 100> stuff;
显式实例化
在模板定义所在的名称空间中,通过关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化
template class ArrayTP<string, 100>;
显式具体化
有时可能需要对特殊类型实例化时,需要对模板进行修改以提供不同的行为,这种情况下可以创建显式具体化
template <> class Classname<specilized-type-name> {...};
在为提供了显式具体化的相应类型创建模板时,将会优先选择具体化版本
部分具体化
部分具体化可以为部分类型参数指定具体的类型
template <class T1, class T2> class Pair {...};
template <class T1> class Pair<T1, int> {...}; //template后面为没有被具体化的类型参数
template <> class Pair<int, int> {...}; //将导致显式具体化
如果有多个模板可以选择,编译器将会选择具体化程度最高的模板,或者为特定类型进行了具体化的版本
模板的友元可以分为三个类型:
非模板友元
template <class T>
class HasFriend {
public:
friend void counts();
friend void report(HasFriend<T> &);
}
对于模板类中的非模板友元函数,必须为要使用的友元定义提供显式具体化
约束模板友元
可以使友元函数本身成为模板,但需要三个步骤:
一个约束模板友元的案例如下:
template <typename T> void counts();
template <typename T> void report(T &);
//模板函数声明
template <typename Type>
class HasFriendT {
...
public:
...
//声明为友元,并对类模板参数Type具体化
friend void counts<Type>();
friend void report<>(HasFriendT<Type> &);
...
}
...
//模板函数定义
template<typename T>
void counts() {...};
template <typename T>
void report(T& t) {...};
非约束模板友元
可以在类内部声明模板以创建友元函数
template <typename T>
class ManyFriend {
...
public:
template<typename C, typename D> friend void show(C &, D &);
}
template<typename C, typename D> friend void show(C &, D &) {...}
对于上面的友元函数的函数调用解释如下:
ManyFriend<double> hfdb;
ManyFriend<int> hfi;
show(hfdb, hfi); //将与具体化 void show &, ManyFriend &> (ManyFriend & c, ManyFriend & d); 匹配
可以将一个类声明为另一个类的友元
class TV {
friend class Remote;
...
}
此时 Remote 类的所有方法都可以影响 Tv 的私有成员
也可以只选定特定的类成员称为另一个类的友元,但这需要在声明顺序上有严格的排序:
class TV;
class Remote {
... //Remote的方法中函数set_chan提及了TV类,因此必须在前面存在声明
}
class TV { //TV的友元函数中提及了Remote类,因此必须在之前存在声明
friend void Remote::set_chan(TV& t, int c); //TV类决定哪个方法成为其友元
}
class TV {
friend class Remote;
public:
void buzz(Remote& r);
...
}
class Remote {
friend TV;
public:
void volup(TV& t) { t.volup(); }
...
}
inline void TV::buzz(Remote& r) {...} //必须在TV方法外定义,并位于Remote声明之后。可以定义在单独方法文件
可以使一个方法成为两个类的友元
class A;
class B {
friend void fun_c(A& a, const B& b);
friend void fun_c(const A& a, B& b);
...
}
class A {
friend void fun_c(A& a, const B& b);
friend void fun_c(const A& a, B& b);
}
inline void fun_c(A& a, const B& b) {...}
inline void fun_c(const A& a, B& b) {...}
可以将类声明嵌套在另一个类中。这一过程不会在外部类中新增成员,只是定义了一种新的类型
外部类的成员方法可以创建和使用嵌套类的对象
若嵌套类被声明在公有部分,那么可以在外部类的外部通过作用域解析运算符使用嵌套类
嵌套类案例
class Queue {
private:
class Node {
public:
Item item;
Node *next;
Node(const Item& i) : item(i), next(nullptr) {}
}
...
}
外部类方法使用嵌套类
bool Queue::enqueue(const Item& i) {
...
Node *add = new Node(Item);
}
方法文件中(外部类的外部)定义构造函数
Queue::Node::Node(const Item& i) : item(i), next(nullptr) {...}
声明位置 | 外部类是否可以使用 | 外部类的派生类是否可以使用 | 外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 通过类限定符 |
通过abort()实现异常终止
abort()函数原型位于头文件 cstdlib 中
典型实现是向标准错误流发送程序异常终止的消息,然后终止程序,同时发送一个随实现而异的值,以告知父进程或操作系统处理失败
不一定刷新文件缓冲区
返回错误码
除了异常终止之外,也可以借助函数返回值来指出问题
函数原本需要返回的结果可以考虑通过传入指针、引用或全局变量来获取
C++中利用异常机制捕获并处理错误,对异常的处理主要由三个部分组成:
try {
...
} catch (Exception e) { //若try块中的语句引发了异常,则会根据抛出的异常的类型执行相应的catch块
...
}
...
throw e; //根据情况抛出异常,使程序沿函数调用序列后退,直到找到包含try块的函数,移交控制权
...
如果引发了异常但没有提供try块或匹配的异常处理,默认情况下程序最终会调用abort()函数
C++11前可以在函数的原型和定义中添加异常规范:
double harm(double a) throw(bad_thing); //告知用户可能需要try块,以及让编译器添加运行阶段检查代码
C++11中建议将异常规范忽略,并引入了新关键字 noexcept 以指出函数不会发生异常
C++通常将信息放在栈中以处理函数调用
程序将调用函数的返回地址放入栈中,当函数被执行完毕时,程序使用这一返回地址来确定接下来从哪里开始继续执行
函数调用将函数的参数放入栈中,被调用的函数创建的新变量也会添加到栈中,这些变量都是自动变量。若函数中调用了另一个函数,则新函数的自动变量也会依次被添加到栈中,以此类推
当函数执行结束时,程序流程将会跳到该函数被调用时存储的地址处(通常是调用它的函数),同时栈顶元素被释放
若函数出现异常而终止,则程序将会释放栈内的内存直到找到一个位于 try 块中的返回地址(及栈解退)
在 exception 头文件中定义了 exception 类,可以将其作为其他自定义异常类的基类
exception 类中提供了虚函数 what() ,返回随实现而异的字符串
此外,C++中定义了很多基于 exception 类的异常类型
stdexcept 头文件中的异常
new 头文件中的 bad_alloc 异常
C++中对于由 new 导致的内存分配问题是让其引发 bad_alloc 异常
空指针和 new
C++中提供了在失败时返回空指针的 new :
int *p1 = new (std::nothrow) int;
int *p2 = new (std::nowthrow) int[500];
若在带有规范异常的函数中引发的异常没有与规范列表中的某种异常匹配,则称为 意外异常
若在没有规范异常的函数或不在函数中引发的异常没有被捕获,则称为 未捕获异常
默认情况下两者都会导致程序异常终止,但C++中提供了修改程序反应的途径
未捕获异常
未捕获异常发生时,程序会首先调用 terminate() 函数(默认调用 abort() 函数),可以通过在程序开头指定该函数将会调用的函数
set_terminate(func_name);
【意外异常】
若发生以外异常,程序将调用 unexcepted() 函数,间接调用 terminate() 函数(默认调用 abort() 函数),可以通过在程序开头指定该函数会调用的函数或要引发的异常,但存在更为严格的限制
栈解退的机制可能导致因函数过早退出而忽略了与 new 相匹配的 delete 语句,这种情况会导致内存泄漏
一种解决方案是在引发异常的函数中添加可以捕获该异常的 catch 块以进行相应的清理,然后重新引发该异常
一种解决方案是使用 智能指针
在包含虚函数的类层次结构中,为了确定基类指针或引用指向的到底是哪种对象,需要引入RTTI
C++中用以支持RTTI的有如下三个机制
dynamic_cast 运算符
该运算符将使用一个指向基类的指针以生成一个指向派生类的指针,否则返回空指针
class Grand {...};
class Superb : public Grand {...};
class Magnificent : public Superb {...};
Grand *pg = new Grand;
Superb *pm = dynamic_cast<Superb*>(pg); //若不能够安全地转换为目标类型,将返回空指针
typeid 运算符
该运算符返回一个指出对象的类型的值,可以接受类名或结果为对象的表达式,返回一个对 type_info 对象的引用
type_info 对象
定义在 typeinfo 头文件中的类,重载了 == 以及 != 运算符以便对类型进行比较
//在类层次结构中进行向上转换
dynamic_cast<type-name> (expression)
//仅作用于类的 const 或 volatile 特征
const_cast<type-name> (expression)
//仅当 type-name 和 expression 可以转换为另一个所属的类型时该转换合法
static_cast<type-name> (expression)
//进行一些依赖于底层的类型转换
reinterpret_cast<type-name> (expression)
//将string对象初始化为s指向的以空字符结束的传统C字符串
string(const char* s);
//将string对象初始化为包含n个元素的string对象,每个元素初始化为字符c
string(size_type n, char c);
//复制构造函数
string(const string& str);
//默认构造函数
string();
//将string对象初始化为[begin, end)区间内的字符
template <class Iter>
string(Iter begin, Iter end);
//将string对象初始化为从对象str的位置pos开始的n个字符
string(const string& str, size_type pos = 0, size_type n = npos);
//C++11
//移动构造函数,将string对象初始化为对象str,并可能修改str
string(string&& str) noexcept;
//将string对象初始化为列表il中的字符
string(initializer_list<char> il);
string str;
cin >> str;
getline(cin, str);
getline(cin, str, '.'); //可选参数,用于指定一个字符作为输入边界,该字符不会存储到str中
比较
string类中对全部6个关系运算符都进行了重载,以便string对象之间或和C-风格的字符串进行比较
find()方法
在字符串中寻找给定子串或字符
//从位置pos开始寻找子串str
size_type find(const string& str, size_type pos = 0) const;
size_type find(const char* s, size_type pos = 0) const;
//从位置pos开始查找s的前n个字符组成的字符串
size_type find(const char* s, size_type pos = 0, size_type n) const;
//从位置pos开始查找字符ch
size_type find(char ch, size_type pos = 0) const;
string::npos
string的静态类成员,表示string对象可以存储的最大字符数,比最大索引值大1
大小的自动调整
当企图将新的内容添加到字符串中时,可能会使用相邻的内存以实现【扩容】,也可能需要分配一个新的内存块,并将新的内容复制到新的内存块中
频繁的复制会带来较大的损耗,因此实际实现中常分配一块原大小两倍的内存块以满足后续的一部分增加内容的请求,避免不断分配新内存块带来的损耗
到C-风格字符串的转换
可以通过c_str()方法将string对象转化为一个C-风格的字符串的指针
其他种类的字符串
常规string基于char类型,但也可以使用基于其他字符类型的字符串
智能指针是行为类似于指针的类对象,可以通过其析构函数的调用实现内存的自动释放
C++中提供了三个智能指针模板:
智能指针模板的定义位于头文件memory中
可以将通过 new 获得的地址赋给智能指针对象
void demo() {
auto_ptr<double> apd(new double);
*apd = 0.1;
return;
} //智能指针过期时,将通过析构函数使用delete自动释放内存
指向同一个对象的智能指针
当两个指针指向同一个对象时可能引发问题,即同一块内存可能被释放两次,对于这种情况可以有如下的解决方案:
定义赋值运算符以执行深复制,使得两个指针指向不同的对象
对于 auto_ptr 以及 unique_ptr 而言,使用了所有权模型,对于特定对象只能由一个智能指针拥有它,可以释放其内存,赋值操作会转让所有权
(所有权的转让使得原来的智能指针变为空指针,这使得对其的访问可能出现问题)
对于 shared_ptr 而言,使用了引用计数的方式,跟踪引用了特定对象的智能指针数。仅当最后一个指针过期时才允许调用delete以释放内存
unique_ptr 和 auto_ptr
auto_ptr<string> p1(new string("auto"));
auto_ptr<string> p2;
p2 = p1; //valid
unique_ptr<string> p3(new string("unique"));
unique_ptr<string> p4;
p4 = p3; //invalid
使用所有权模型的智能指针失去所有权后不再指向有效的数据,如果程序后续仍对其进行访问和其他操作存在较大的安全隐患
auto_ptr 允许这样的所有权转换,在C++11中被摈弃
unique_ptr 有更加严格的控制,在进行赋值操作时,允许它获得临时右值的所有权,但对于可能导致留存的指针悬挂的源unique_ptr的赋值将会被编译器禁止
unique_ptr<string> up1(new string("unique"));
unique_ptr<string> up2;
up2 = up1; //not allowed
unique_ptr<string> p3 = unique_ptr<string>(new string("temporary")); //allowed
但通过移动构造函数和右值引用可以安全地赋值
此外,unique_ptr 有用于数组的变体( auto_ptr 以及 shared_ptr 都仅使用delete)
当 unique_ptr 为右值时,可以将其赋给 shared_ptr 对象
C++程序将输入和输出看做字节流,输入时程序从输入流中抽取字节,输出时程序将字节插入输出流中。流充当了程序和流源以及输出源之间的桥梁,使得C++程序处理输入和输出可以独立于其来源于去向。输入输出管理包含两步:
使用缓冲区可以更加高效地处理输入和输出——缓冲区是作为中介的内存块,在输入输出时临时存储信息,以缓和硬件操作的大量开销。输出时,程序首先填满缓冲区,然后将整块数据传输给硬盘,随后清空缓冲区(刷新);输入时,可以借助缓冲区暂存用户输入,以在传送给程序前可以进行适当更正,随后一次性将内容输入,并刷新缓冲区
iostream文件中包含了专门设计以实现和管理流及缓冲区的类:
包含 iostream 文件将会自动创建8个流对象(4种,每种两个分别用于宽窄字符流)
ostream 类重载了 << 运算符作为插入运算符,其可以识别C++中所有的基本类型,也可以识别指针,如可以输出地址或者C-风格的字符串
对 operator<< 的重载返回一个 ostream 对象,使得可以拼接输出
//显示一个字符
ostream& put(char);
//显示字符串中的n个字符,即使超出字符串边界
basic_ostream<charT, traits>& write(const char_type* s, streamsize n);
ostream类对cout对象处理的输出进行缓冲,因而输出不会立即发送到目标地址,而会先缓冲在缓冲区中,直到缓冲区被填满或主动刷新缓冲区
可以在输出时通过控制符 flush 或 endl 主动刷新缓冲区(直接显示),其中 endl 控制符刷新缓冲区后还会插入一个换行符
ostream 的插入运算符将值转换为文本格式,转换结果一定程度上可以配置:
修改计数系统
cout << hex; //程序在此后将以十六进制形式打印整数值
调整字段宽度
cout.width(); //返回当前设置的字段宽度
cout.width(12); //设置cout接下来的下一个输出项的字段宽度(右对齐),然后回复默认值
填充字符
cout.fill('*'); //指定填充字段中未使用部分的字符,默认为空格
设置浮点显示精度(包括整数部分在内的总位数)
C++中浮点数显示时默认精度为6位,但结尾的0不会显示
cout.precision(2); //调整精度到2位
格式化特性的控制
通过在方法 setf 填充对应的格式常量可以控制输出时的一些格式化特性:
常量 | 含义 |
---|---|
ios_base::boolalpha | 输入和输出bool值 |
ios_base::showbase | 对输出使用基数前缀 |
ios_base::showpoint | 显示末尾的小数点和0 |
ios_base::uppercase | 对16进制输出使用大写字母表示 |
ios_base::showpos | 在正数前加上+ |
同时也可以使用有两个参数的 setf 方法设置由多位控制的格式选项:
...
标准控制符
C++中提供了多个控制符,可以等效于调用 setf 并提供正确的参数
头文件 iomanip
该头文件中提供了其他的一些简化的控制符提供上述服务
cout << setprecision(length) << ...;
cout << setw(length) << ...;
cout << setfill(character) << ...;
cin对象将标准输入表示为字节流,它从输出流中抽取字符,抽取过程会涉及类型转换
istream 类重载了 >> 运算符,可以识别C++中的许多基本类型
这些运算符函数被称为格式化输入函数(可以将输入数据从输入流中抽取,转换为目标指定的格式)
抽取运算符在查看输入流时会跳过空白(空格、换行符和制表符),直到遇到非空白字符
运算符将会从空白字符开始,读取一个指定类型的数据,直到第一个与目标类型不匹配的字符
若 istream 对象的错误状态被设置(如输入不成功),则 if 或 while 语句将会判定该对象为 false
cin 或 cout 对象包含一个描述流状态的数据成员(继承自 ios_base),由三个 ios_base 元素组成:
(goodbit:另一种表示0的方法)
主动设置状态的方法
cin.clear(); //清除所有状态位,或设置指定的状态位并清除其他状态位
cin.setstate(); //设置指定的状态位,其他参数不受影响
I/O和异常
cin.exceptions(); //返回一个三位的位字段,分别对应于三个流状态位,默认值为goodbit
cin.exceptions(badbit);
cin.exceptions(badbit | eofbit);