new表达式实际执行了三步操作:
- new表达式调用一个名为
operator new
(或者operator new[]
)的标准库函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组) - 编译器运行相应的构造函数以构造这些对象,并为其传入初始值
- 对象被分配了空间并构造完成,返回一个指向该对象的指针
string *sp = new string("a value"); // 分配并初始化一个string对象
string *arr = new string[10]; // 分配10各默认初始化的string对象
delete表达式实际执行了两步操作:
- 对sp所指的对象或arr所指的数组中的元素执行对应的析构函数
- 编译器调用名为
operator delete
(或者operator delete[]
)的标准库函数释放内存空间
delete sp;
delete [] arr;
1. operator new 接口和operator delete接口
- operator new用在对象构造之前而operator delete 用在对象销毁之后,所以new和delete必须是静态的,而且它们不能操纵类的任何数据成员。
- 对于operator new函数或者operator new[]函数来说,它的返回类型必须是void*,第一个形参的类型必须是size_t且该形参不能含有默认实参
void *operator new(size_t, void*);
不允许重新定义- 对于operator delete函数或者operator delete[]函数来说,它的返回类型必须是void,第一个形参的类型必须是void*
- 当我们将operator delete 或 operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。该形参的初始值是第一个形参所指对象的字节数
- 我们不能改变new 运算符和delete运算符的基本含义
2. malloc函数和free函数
- C++从C语言中继承的函数,头文件
cstdlib
- malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或者返回0表示分配失败
- free函数接受一个void*,他是malloc返回的指针的副本,free将相关内存返回给系统
void* operator new(size_t size){
if(void* mem = malloc(size)){
return mem;
}else{
throw bad_alloc();
}
}
void operator delete(void* mem) noexcept {
free(mem);
}
- todo
运行时类型识别(run-time type identification, RTTI)的功能由两个运算符实现:
- typeid运算符,用于返回表达式的类型
- dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用
这两个运算符特别适用于一下情况:想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。
使用RTTI必须要加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。
dynamic_cast运算符的使用形式如下:
// 1. type必须是一个类类型,通常情况下该类型应该含有虚函数
dynamic_cast<type*>(e) // e 必须是一个有效的指针
dynamic_cast<type&>(e) // e 必须是一个左值
dynamic_cast<type&&>(e) // e 不能是左值
e的类型必须符合以下三个条件中的任意一个:
- e的类型是目标type的公有派生类
- e的类型是目标type的公有基类
- e的类型是目标type的类型
如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0
如果转换目标是引用类型并且失败了,则dymaic_cast运算符将抛出一个bad_cast异常
1. 指针类型的dymaic_cast
- 可以对一个空指针执行dymaic_cast,结果是所需类型的空指针
- 在条件部分执行dymaic_cast操作可以确保类型转换和结果检查在同一条表达式中完成
if(Derived *dp = dynamic_cast<Derived*>(bp)){
// 使用 dp 指向的Derived对象
}else{
// 使用 bp 指向的Base对象
}
2. 引用类型的dymaic_cast
- 当对引用的类型转换失败时,程序抛出一个名为
std::bad_cast
的异常,该异常定义在typeinfo
标准库头文件中
void f(const Base& b){
try{
const Derived& d = dymaic_cast<const Derived&>(b);
// 使用b引用的Derived对象
} catch(bad_cast){
// 处理类型转换失败的情况
}
}
1. 使用typeid运算符
Derived* dp = new Derived;
Base* bp = dp; // 两个指针都指向Derived对象
// 在运行时比较两个对象的类型
if(typeid(*bp) == typeid(*dp)){
// bp 和 dp 指向同一类型的对象
}
// 检查运行时类型是否是某种指定的类型
if(typeid(*bp) == typeid(Derived)){
// bp实际指向Derived对象
}
- 当 typeid 作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型
1. 类的层次关系
class Base{
friend bool operator==(cosnt Base&, const Base&);
public:
// Base 的接口成员
protected:
virtual bool equal(const Base&) const;
};
class Derived : public Base{
public:
// Derived 的接口成员
protected:
bool equal(const Base&) const;
};
2. 类型敏感的相等运算符
bool operator==(const Base& lhs, const Base& rhs){
// 如果 typeid 不相同,返回 false;否则虚调用 equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
3. 虚equal函数
bool Derived::equal(const Base& rhs) const{
auto r = dynamic_cast<const Derived&>(rhs);
// 执行比较两个Derived对象的操作并返回结果
}
4. 基类equal函数
bool Base::equal(const Base& rhs) const{
// 执行比较Base对象的操作
}
- todo
C++11:限定作用域的枚举类型(scoped enumeration)
enum class open_modes {input, output, append};
不限定作用域的枚举类型(unscoped enumeration)
enum color {red, yellow, green};
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
1. 枚举成员
enum color {red, yellow, green}; // 不限定作用域的枚举类型
enum stoplight {red, yellow, green}; // 错误:重复定义了枚举成员
enum class peppers {red, yellow, green};// 正确:枚举成员被隐藏了
color eyes = green; //正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green; // 错误:peppers 的枚举成员不在有效的作用域中
color hair = color::red;
peppers p2 = peppers::red;
- 默认情况下,枚举值从0开始,依次加1,也可指定专门的值
- 枚举值不一定唯一
- 如果没有显式地提供初始值,则当前枚举成员地值等于之前枚举成员的值加1
- 枚举成员是const
enum class intTypes{
charTyp = 8, shortTyp = 16, intTyp = 16,
longTyp = 32, long_longType = 64
};
2. 和类一样,枚举也定义新的类型
- 要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象
open_modes om = 2; // 错误:2 不属于类型 open_modes
om = open_modes::input; // 正确:input 是 open_modes 的一个枚举成员
- 一个不限定作用于的枚举类型的对象或枚举成员自动地转换成整型
int i = color::red; // 正确
int j = peppers::red; // 错误:限定作用域地枚举类型不会进行隐式转换
3. 指定 enum 的大小
- 默认情况下限定作用域的enum成员类型是int
enum intValues : unsigned long long{
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL
};
4. 枚举类型的前置声明
- 在C++11新标准中,可以提前声明 enum
- enum 的前置声明,必须指定其成员的大小
enum intValues : unsigned long long; // 不限定作用域的,必须指定成员类型
enum class open_modes: // 限定作用域的枚举类型可以使用默认成员类型 int
5. 形参匹配与枚举类型
- 要想初始化一个enum对象,必须使用该enum类型的另一个对象或者它的一个枚举成员
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main()
{
Tokens curTok = INLINE;
ff(128); // 精确匹配 ff(int)
ff(INLINE); // 精确匹配 ff(Tokens)
ff(curTok); // 精确匹配 ff(Tokens)
return 0;
}
- 不能直接将整型值传给enum形参,但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); // 调用 newf(int)
newf(uc); // 调用 void newf(unsigned char)
成员指针(pointer to member)是指可以指向类的非静态成员的指针。
class Screen{
public:
typedef string::size_type pos;
char get_cursor() const {
return contents[cursor];
}
char get() const;
char get(pos ht, pos wd) const;
private:
string contents;
pos cursor;
pos height, width;
};
- 声明数据成员指针
// pdata可以指向一个常量(非常量)Screen对象的string成员
const string Screen::*pdata;
- 初始化一个成员指针(或向它赋值)
pdata = &Screen::contents;
- C++11新标准中声明成员指针最简单的方法:使用auto或decltype
auto pdata = &Screen::contents;s
1. 使用数据成员指针
- 当初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据
Screen myScreen, *pScreen = &myScreen;
// .* 解引用pdata以获得myScreen对象的contents成员
auto s = myScreen.*pdata;
// ->* 解引用pdata以获得pScreen所指对象的contents成员
s = pScreen->*pdata;
2. 返回数据成员指针的函数
- 数据成员一般情况下是私有的,通常不能直接获得数据成员的指针
```cpp
class Screen{
public:
// data 是一个静态成员,返回一个成员指针
static const string Screen::*data(){
return &Screen::contents;
}
};
- 调用data函数,将得到一个成员指针
// data() 返回一个指向Screen类的contents成员的指针
const string Screen::*pdata = Screen::data();
- 获得 myScreen 对象的 contents成员
auto s = myScreen.*pdata;
// pmf 是一个指针,它可以指向Screen的某个常量成员函数
// 前提是该函数不接受任何实参,并且返回一个char
auto pmf = &Screen::get_cursor;
- 如果成员存在重载问题,必须显式地声明函数类型以明确指出想要使用的是哪个函数
- 出于优先级的考虑,Screen::*pmf2两端的括号必不可少
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
- 在成员函数和指向该成员的指针之间不存在自动转换规则
// pmf 指向一个Screen成员,该成员不接受任何实参且返回类型为char
pmf = &Screen::get; // 必须显式地使用取地址运算符
//pmf = Screen::get; // 错误
1. 使用成员函数指针
- 括号必不可少,因为调用运算符的优先级要高于指针指向成员运算符的优先级
Screen myScreen, *pScreen = &myScreen;
// 通过pScreen所指的对象调用pmf所指的函数
char c1 = (pScreen->*pmf)();
// 通过myScreen对象将实参 0,0 传递给含有两个形参的get函数
char c2 = (myScreen.*pmf2)(0, 0);
2. 使用成员指针的类型别名
- 使用类型别名或 typedef 可以让成员指针更容易理解
// Action 是一种可以指向Screen成员函数的指针,它接受两个pos实参,返回一个char
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;
- 将指向成员函数的指针作为某个函数的返回类型或形参类型
// action 接受一个Screen的引用,和一个指向Screen成员函数的指针
Screen& action(Screen&, Action = &Screen::get);
Screen myScreen;
// 等价的调用
action(myScreen);
action(myScreen, get);
action(myScreen, &Screen::get);
3. 成员指针函数表
class Screen{
public:
Screen& home();
Screen& forward();
Screen& back();
Screen& up();
Screen& down();
// Action是一个指针,可以用任意一个光标移动函数对其赋值
using Action = Screen& (Screen::*)();
enum Directions {HOME, FORWARD, BACK, UP, DOWN};
Screen& move(Directions);
private:
static Action Menu[]; //函数表
};
Screen& Screen::move(Direction cm){
return (this->*Menu[cm])();
}
Screen myScreen;
myScreen.move(Screen::HOME);
myScreen.move(Screen::DOWN);
Screen::Action Screen::Menu[] = {
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down
};
- 成员指针不是一个可调用对象,这样的指针不支持函数调用运算符
auto fp = &string::empty; // fp 指向string的empty函数
// 错误,必须使用.*或->*调用成员指针
//find_if(svec.begin(), svec.end(), fp);
1. 使用function生成一个可调用对象
- 标准模板库function
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);
2. 使用mem_fn生成一个可调用对象
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
3. 使用bind生成一个可调用对象
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见
1. 声明一个嵌套类
class TextQuery{
public:
class QueryResult;
};
2. 在外层类之外定义一个嵌套类
class TextQuery::QueryResult{
friend ostream& print(ostream&, const QueryResult&);
public:
// 嵌套类可以直接使用外层类的成员,无需对该成员的名字进行限定
QueryResult(string, shared_ptr<set<line_no>>, shared_ptr<vector<string>>);
};
3. 定义嵌套类的成员
TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p, shared_ptr<vector<string>> f)
: sought(s), lines(p), file(f) { }
4. 嵌套类的静态成员定义
int TextQuery::QueryResult::static_mem = 1024;
5. 嵌套类作用域中的名字查找
// 返回类型必须指明 QueryResult 是一个嵌套类
TextQuery::QueryResult TextQuery::query(const string& sought) const{
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc = wm.find(sought);
if(loc == wm.end()){
return QueryResult(sought, nodata, file);
}else{
return QueryResult(sought, loc->second, file);
}
}
6. 嵌套类和外层类是相互独立的
联合(union)
- 一个union可以有多个数据成员,但在任意时刻只有一个数据成员可以有值
- 分配给一个union对象的存储空间至少要容纳它的最大的数据成员
- union不能含有引用类型的成员
- 默认情况下,union的成员是公有的
- union中不能含有虚函数
1. 定义union
// Token类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union Token{
char cval;
int ival;
double dval;
};
2. 使用union类型
Token first_token = {'a'}; // 初始化 cval成员
Token last_token; // 未初始化的Token对象
Token* pt = new Token; // 指向一个未初始化的Token对象的指针
last_token.cval = 'z';
pt->ival = 42;
3. 匿名union
- 匿名union不能包含受保护的成员或私有成员,也不能定义成员函数
union {
char cval;
int ival;
double dval;
}; // 定义了一个未命名的对象,可以直接访问它的成员
cval = 'c';
ival = 42;
4. 含有类类型成员的union
- 当将 union的值改为类类型成员对应的值时,必须运行该类型的构造函数;当将类类型成员的值改为一个其他值时,必须运行该类型的析构函数
- 当union包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制函数
- union含有类类型的成员,并且该类型自定义了默认析构函数或拷贝控制函数,则编译器将为union合成对应的版本并将其声明为删除的
5. 使用类管理union成员
- 通常把含有类类型成员的 union 内嵌在另一个类当中
class Token{
public:
Token() : tok(INT), ival(0) { }
Token(const Token& t) : tok(t.tok) {copyUnion(t);}
Token& operator=(const Token& t);
// 如果 union 含有一个string成员,则必须销毁它
~Token(){
if(tok == STR){
sval.~string();
}
}
Token& operator=(const string&);
Token& operator=(char);
Token& operator=(int);
Token& operator=(double);
private:
enum {INT, CHAR, DBL, STR} tok; // 判别式
union{ // 匿名union
char cval;
int ival;
double dval;
string sval;
};
// 检查判别式,然后酌情拷贝 union成员
void copyUnion(const Token&);
};
6. 管理判别式并销毁string
Token& Token::operator=(int i){
if(tok == STR){
sval.~string();
}
ival = i;
tok = INT;
return *this;
}
- string 版本必须管理与string类型有关的转换
Token& Token::operator=(const string& s){
if(tok == STR){
sval = s;
}
else{
new(&sval) string(s); // 定位new表达式
}
tok = STR;
return *this;
}
7. 管理需要拷贝控制的联合成员
void Token::copyUnion(const Token& t){
switch(t.tok){
case Token::INT: ival = t.ival;
break;
case Token::CHAR: cval = t.cval;
break;
case Token::DBL: dval = d.cval;
break;
case Token::STR: new(&sval) string(t.sval);
break;
}
}
- 赋值运算符必须处理string成员的三种可能情况:左侧运算对象和右侧运算对象都是string、两个运算对象都不是string、只有一个运算对象时string
Token& Token::operator=(const Token& t){
if(tok == STR && t.tok != STR){
sval.~string();
}
if(tok == STR && t.tok == STR){
sval = t.sval;
}else{
copyUnion(t);
}
tok = t.tok;
return *this;
}
- 局部类:定义在某个函数内部的类
- 局部类的所有成员(包括函数在内)都必须完整定义在类的内部
- 在局部类中不允许声明静态数据成员
1. 局部类不能使用函数作用域中的变量
- 局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员
int a, val;
void foo(int val){
static int si;
enum Loc {a = 1024, b};
struct Bar{
Loc locVal; //正确:使用一个局部类型名
int barVal;
void fooBar(Loc l = a){ // 正确:默认实参时 Loc::a
//barval = val; // 错误:val 是 fol 的局部变量
barval = ::val; // 正确:使用一个全局对象
barval = si; // 正确:使用一个静态局部对象
locVal = b; // 正确:使用一个枚举成员
}
};
}
2. 常规的访问保护规则对局部类同样适用
- 外层函数对局部类的私有成员没有任何访问特权
3. 局部类中的名字查找
- 在声明类的成员时,必须先确保用到的名字位于作用域中,然后再使用改名字
4. 嵌套的局部类
- 嵌套类的定义可以出现在局部类之外
- 嵌套类必须定义在与局部类相同的作用域中
void foo(){
class Bar{
public:
class Nested;
};
class Bar::Nested{
};
}
不可移植(nonportable)的特性:因机器而异的特性,如算术类型的大小在不同机器上不一样
- 位域(从C语言继承)
- volatile限定符(从C语言继承)
- 链接指示(C++新增)
- 类可将其数据成员定义成位域(bit-field),一个位域中含有一定数量的二进制位
- 位域在内存中的布局是与机器相关的
- 位域的类型必须是整型或枚举类型,通常使用无符号类型保存
- 如果可能,在类的内部连续定义的位域压缩在同一整数的相邻位,从而提供存储压缩
- 取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域
typedef unsigned int Bit;
class File{
Bit mode: 2; // mode 占2位
Bit modified: 1; // modified 占1位
Bit prot_owner: 3; // prot_owner 占3位
Bit prot_group: 3;
Bit prot_world: 3;
public:
enum modes {READ = 01, WRITE = 02, EXECUTE = 03};
File& open(modes);
void close();
void write();
void isRead() const;
void setWrite();
};
1. 使用位域
- 通常使用内置的位运算符操作超过1位的位域
void File::write(){
modified = 1;
}
void File::close(){
if(modified){
}
}
File& File::open(File::modes m){
mode |= READ; // 按默认方式设置READ
if(m & WRITE){ // 如果打开了READ 和 WRITE
// 按照读写方式打开文件
}
return *this;
}
- 如果类定义了位域成员,通常也会定义一组内联的成员函数以检验或设置位域的值
inline bool File::isRead() const {return mode & READ;}
inline void File::setWrite() {mode |= WRITE;}
- volatile 的确切含义与机器有关,只能通过阅读编译器文档来理解
- 当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile
- 只有volatile的成员函数才能被volatile的对象调用
// 用法和 const 相似
volatile int dispaly_register; // 该int值可能发生改变
volatile Task* curr_task;
volatile int iax[max_size];
volatile Screen bitmapBuf;
volatile int v;
int* volatile vip; // vip 是一个volatile指针,指向int
volatile int* ivp; // ivp 时一个指针,指向一个 volatile int
volatile int* volatile vivp;
//int* ip = &v; //错误:必须使用指向volatile的指针
ivp = &v;
vivp = &v;
1. 合成的拷贝对volatile对象无效
- 合成的成员接受的形参类型是(非volatile)常量引用
- 不能使用合成的拷贝/移动构造函数及赋值运算符初始化volatile对象或从volatile对象赋值
// 自定义拷贝或移动操作
class Foo{
public:
Foo(const volatile Foo&);
// 将一个 volatile 对象赋值给一个非 volatile 对象
Foo& operator=(volatile const Foo&);
// 将一个 volatile 对象赋值给一个 volatile 对象
Foo& operator=(volatile const Foo&) volatile;
};
linkage directive
- C++程序有时需要调用其他语言编写的函数
- 其他语言中的函数名字必须在C++中进行声明,并且该声明必须指定返回类型和形参
- 要想把C++代码和其他语言编写的代码放在一起使用,要求必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的
1. 声明一个非C++的函数
- 链接指示可以是单个的或复合的
- 链接指示不能出现在类定义或函数定义的内部
- 同样的链接指示必须在函数的每个声明中都出现
// 可能出现在C++头文件<cstring>中的链接指示
// 单语句链接指示
extern "C" size_t strlen(const char*);
// 复合语句链接指示
extern "C"{
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
// extern "Ada", extern "FORTRAN"
2. 链接指示与头文件
- 可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接
extern "C"{
#include <string.h> //操作C风格字符串的C函数
}
3. 指向extern "C"函数的指针
- 对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示
- 指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示
extern "C" void(*pf)(int);
- 一个指向C函数的指针不能用在执行初始化或赋值操作后指向C++函数
void (*pf1)(int); // 指向一个C++函数
extern "C" void(*pf2)(int); //指向一个C函数
// pf1 = pf2; // 错误:pf1 和 pf2 的类型不同
4. 链接指示对整个声明都有效
- 当使用链接指示时,不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效
// f1是一个C函数,它的形参是一个指向C函数的指针
extern "C" void f1(void(*)(int));
- 如果希望给C++函数传入一个指向C函数的指针,则必须使用类型别名
// FC 是一个指向C函数的指针
extern "C" typedef void FC(int);
// f2 是一个C++函数,该函数的形参是指向C函数的指针
void f2(FC*);
5. 导出C++函数到其他语言
- 通过使用链接指示对函数进行定义,可以令一个C++函数在其他语言编写的程序中可用,编译器将为该函数生成适合于指定语言的代码
// calc 函数可以被C程序调用
extern "C" double calc(double dparm) {/*...*/}
- 需要在C和C++中编译同一个源文件,在编译C++版本的程序时预处理器定义
__cplusplus
#ifdef __cplusplus
// 正确:我们正在编译C++程序
extern "C"
#endif
int strcmp(const char*, const char*);
6. 重载函数与链接指示
- C语言不支持重载
// 错误:两个 extern "C" 函数的名字相同
//extern "C" void print(const char*);
//extern "C" void print(int);
- 如果在一组重载函数中有一个是C函数,则其余的必定都是C++函数
class SmallInt {/*...*/};
class BigNum {/*...*/};
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);