1).不能直接应用标准内存管理机制。
new
将对象放置在特定的内存空间中。new
和delete
运算符,来控制内存分配的过程。1).尽管说是重载,但是重载它们和重载其他的运算符过程大不相同。
2).了解new
和delete
表达式的工作机理。
{
// ---------------new
string *sp = new string("a value");//分配初始化一个string对象
string *arr = string[10];//分配并默认初始化10个string对象
// 实际的操作执行了三部
// 1. new表达式调用一个名为operator new或者operator new[]的标准库函数,该函数分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象或者对象的数组
// 2. 编译器运行相应的构造函数以构造这些对象,并传入初始值
// 3. 对象被分配了空间并且构造完成,返回指向该对象的指针
// --------------delete
delete sp;//销毁*sp,然后释放sp指向的内存空间
delete [] arr;//销毁数组中的元素,然后释放对应的内存空间
// 实际执行了两步操作
// 1. 执行 sp指向的对象或者arr所指向数组中的元素 的析构函数
// 2. 编译器调用名称为operator delete或者operator delete []的标准库函数释放内存空间。
// 如若应用程序希望控制内存分配的过程
// 则需要定义自己的operator new和operator delete函数
// 即使标准库已经存在这两个函数的定义,我们仍旧可以定义自己的版本
// 编译器不会对这种重复定义提出异议,相反,编译器将会使用我们自定义的版本代替标准库的版本
// 一旦自定义了全局的operator new和operator delete函数,我们就担负其控制动态内存分配的职责。
// 这两个函数必须是正确的,它关乎程序处理过程的至关重要的一部分
// 应用程序可以在全局作用域定义operator new和delete,也可以将它们定义为成员函数
// 当编译器发现new或者delete表达式后,将在程序中查找可供调用的operator函数。
// 如何查找?
// 如果被分配(释放的)对象是类类型,则编译器首先在类及其基类的作用域中查找。(类的作用域。)
// 此时如果该类含有operator new或者和delete函数成员,则相应的表达式掉用这些成员
// 否则编译器会在全局作用域中查找匹配的函数,如若找到了用户自定义的版本,则使用该版本
// 如果没有,执行标准库的版本
// 我们可以使用作用域运算符,使new或者delete表达式忽略定义在类中的函数
// 直接执行在全局中用于中的版本,
// 使用作用域运算符就可以
// ::new,::delete
}
3).operator new
和operator delete
接口
{
// 标准库定义了operator new函数operator delete函数的八个重载版本。
// 前四个版本可能会抛出bad_alloc异常
// 后四个不会抛出异常
void* operator new(size_t);//分配一个对象
void* operator new[](size_t);//数组
// 析构函数不允许抛出异常。书本有误
void* operator delete(void*) noexcept;//释放一个对象
void* operator delete[](void*) noexcept;//数组
//
void* operator new(size_t, nothrow_t&) noexcept;
void* operator new[](size_t, nothrow&) noexcept;
void* operator delete(size_t, nothrow&) noexcept;
void* operator delete [] (size_t, nothrow&) noexcept;
// 类型,nothrow_t是定义在头文件new中的一个struct
// 这个类型不包含任何成员
// new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本
// 与析构函数类似,operator delete也不允许抛出异常
// 必须使用noexcept异常说明符号指定其不抛出异常
// 应用程序可以自定义上面函数版本的任意一个,前提是自定义版本必须位于全局作用域或者类作用域。
// 当我们将上述运算符函数定义成类的成员时,它们是隐式静态的
// 我们无需显式地声明static,这样做也不会引发错误
// 因为operator new用在对象构造之前
// operator delete用在对象销毁之后
// 所以这两个成员必须是静态的,而且
// 不可以操纵任何数据成员
//--------对于operator new或者new[],
// 它们的返回值类型必须是void*,第一个形参类型必须是size_t,且该形参不能有默认实参
// 当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参
// 当调用[]时,传入函数的则是存储数组中所有元素所需要的空间
// -----------如果我们想要自定一operator new函数,可以为他提供额外的形参
// 此时,用到这些自定义函数的new表达式必须是使用new的定位模式,将实参传给新增的形参。
// 尽管我们可以自定义具有任何形参的operator new
// 但是,
void* operator new(size_t, void*);
// 只供给标准库使用,不能被用户重新定义
// ------------对于operator delete或者delete[]
// 它们的返回类型必须时void
// 第一个形参必须是void*,执行一条delete表达式将调用相应的operator函数
// 并用指向待释放内存的指针来初始化void*形参
// 当我们将operator delete 或者delete[]定义成类的成员时,
// 该函数可以包含另外一个类型为size_t的形参
// 此时,该形参的初始值是第一个形参所指对象的字节数
// size_t形参可用于删除继承体系中的对象
// 如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别
// 而且,实际运行的operator delete的函数版本也由对象的动态类型决定。
// ------------
// operator new和operator delete和一般的operator函数不一样
// 这两个函数并没有重载new表达式和delete表达式
// 实际上,我们根本无法自定义new表达式或者delete表达式的行为
// 它们的执行过程如之前所述,我们无法改变
// 我们提供的operator new或者operator delete函数的目的在于,改变内存分配的方式。
}
4).malloc
函数和free
函数
operator new
和operator delete
后,这两个函数必须以某种方式执行分配内存与释放内存的操作。malloc
和free
的函数,这是从c语言中继承而来的函数,定义在头文件cstdlib
malloc
,接受一个表示待分配字节数的size_t
返回指向分配空间的指针,或者返回0表示分配空间失败。free
,接受一个void*
,它是malloc
返回指针的副本,free
将相关的内存返回给系统。调用free(0)
没有任何意义。{
// ------编写operator new
void* operator new(size_t size) {
if (void *mem = malloc(size))
return mem;
else
throw bad_alloc();
}
// -----------编写operator delete
void operator delete(void *mem) noexcept {
free(mem);
}
}
练习
operator new/delete
代替,allocator
类的操作。需要显式地指出。allocator
类使用operator new/delete
来分配和释放内存空间。1).普通代码调用标准库的两个普通函数,operator new,operator delete;
。尽管它们一般用于new
或者delete
表达式。
allocator
类,还不是标准库的一部分。应用程序如果想要把内存分配和初始化分离开的话,需要调用operator new或者operator delete
。这两个函数的行为和allocator
的allocate
成员和deallocate
成员非常的类似。它们负责分配和释放内存空间,但是不会构造和销毁对象。allocator
不同的是,对于operator new
分配的内存空间,我们无法使用construct
函数构造对象。而是,我们应该是使用定位new
形式来构造对象。construct
和以下介绍的版本的定位new
的功能很类似,都是用来构造一个对象的,但是实际上并不分配内存(与一般的new
表达式不一样。)new
为分配函数提供了额外的信息。{
// 我们可以使用定位new传递一个地址,此时定位new的形式如下
new(place_address) type
new(place_address) type (initializers)
new(place_address) type [size]
new(place_address) type [size] {braced initializer list}
// 其中place_address必须是一个指针,同时在initializers中提供一个(可以为空)以逗号分割的初始值列表,该初始值列表将用于构造新分配的对象
// 当仅通过一个地址值调用时,定位new使用operator new(size_t, void*)“分配”它的内存
// 这是我们无法自定义的operator new版本
// 该函数不分配任何内存,它只是简单地返回指针实参,然后由new表达式负责在指定的地址初始化对象以完成整个工作
// 事实上,定位new允许我们在一个特定的,预先分配的内存上构造对象
// 当只传入一个指针类型的实参时,定位new表达式构造对象但是不会分配内存。定位new只是用来构造对象。
// ----------------定位new和construct的异同
// 尽管很多时候使用定位new和allocator的construct成员非常类似,但是它们之间也有一个重要的区别
// 我们传递给construct的指针必须指向同一个allocator对象分配的空间
// 但是传递给定位new的指针无需指向operator new分配的内存
// 实际上,传递给定位new表达式的指针甚至不需要指向动态内存。?
}
2).显式的析构函数调用
new
和allocate
的construct
类似一样,对于析构函数的显式调用也与使用destroy
很类似。{
string *sp = new string("a value");
sp->~string();
// 和调用destroy类似,调用析构函数可以清除给定的对象但是不会释放该对象所在的空间,如果需要,我们可以重新使用该空间
// 意犹未尽.....
}
1).该功能由两个运算符实现
typeid
,用于返回表达式的类型dynamic_cast
,用于将基类的指针或引用安全地转换成派生类的指针或引用。2).当我们将这两个运算符用于某一种类型的指针或引用时,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型(实际类型)。
RTTI
(run_time_type_identification)运算符。但是使用它有更多的潜在风险。必须清楚地知道转换的目标类型并且检查类型转换是否被成功执行。RTTI
必须加倍小心,在可能的情况下,最好定义虚函数而不是直接接管类型管理的重任。1).
{
// 使用形式
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
// 其中type必须是一个 类类型
// 并且通常情况下,该类型含有虚函数
// 第..种形式
// 1. e必须是一个有效指针
// 2. e必须是一个左值
// 3. e不能是左值
// 在上面的所有形式中,e的实际类型必须符合以下三个条件中的任意一个
// 1. e的类型是目标type的公有派生类
// 2. e的类型是目标type的公有基类
// 3. e的类型就是目标type的类型
// 如果符合,则类型转换 可能 成功,否则转换失败
// 转换成功还需要保证,e实际指向的对象是指针类型的派生类!!!
// ---------转换失败时
// 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0
// 如果是引用,并且失败,它将会抛出一个bad_cast的异常,该异常定义在头文件`typeinfo`中
// -------------指针类型的dynamic_cast
// 以下例子很混乱,不建议参考。翻译版本很混乱..........
// Base类至少含有一个虚函数,Derived是Base的public派生类
// 如果有一个指向Base的指针bp
// 则我们可以在运行时将它转换成指向Derived的指针。实际上能不能成功,取决于bp的实际指向对象。
if (Derived *dp = dynamic_cast<Derived*>(bp)) {
// 使用dp指向Derived对象
} else {
// bp仍然指向Base对象,使用dp所指向的Base对象
}
// 如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指向的Derived对象,
// 此时,if语句内部使用Derived操作的代码是安全的
// 否则转换的结果是0,dp具有0意味着
// if语句条件失败
// ----------如若bp指向的是一个base对象,可以转换成功吗?不可以
// 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针
// 在条件中,我们定义了dp,
//在一个操作中,同时完成类型转换和类型检查两个任务
// 并且if外部是不能访问dp的
// 一旦转换失败,即使后续的代码忘记做相应的判断,也不会接触到这个未绑定的指针。
// ------------引用类型的dynamic_cast
// 引用的错误表示方式和指针不一样,因为没有所谓的空引用
void f(const Base &b) {
try {
const Derived &d = dynamic_cast<const Derived&>(b);
} catch (bad_cast) {
//处理类型转换的错误
}
}
}
练习
RTTI
选项。dynamic_cast
将基类指针转换为该类型的指针,使用该成员。所以,如果无法为基类添加虚函数时,可以使用dynamic_cast
来代替虚函数。1).
{
// 它允许程序向表达式提问,你的 实际对象 是什么类型?
// -------表达形式
typeid(e);
// 其中e可以是任意表达式或者类型的名字。
// 操作结果是一个const对象的引用
// 该对象的类型是标准库类型type_info或者type_info的公有派生类型
// type_info类定义在头文件typeinfo中
// -----------规则
// 1. 顶层const被忽略
// 2. 如果表达式是一个引用,则typeid返回该引用所引对象的类型
// 3. 不过当typeid作用于数组或函数时,并不会向执行指针的转换
// 当运算对象不属于类类型或者是一个不包含任何虚函数的类时
// typeid运算符指示的是运算对象的静态类型
// 而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运算时才会求得(就是动态类型。)
// -------------使用typeid运算符
//使用typeid
// 1. 用来比较两条表达式的类型是否相同
// 2. 比较一条表达式的类型是否与指定类型相同
Derived *dp = new Derived;
Base *bp = dp;
if (typeid(*bp) == typeid(*dp))
// 实际指向的是同一类型
if (typeid(*bp) == typeid(Derived))
// 是否实际指向Derived对象。
// typeid应该作用于对象,因此要解引用
if (typeid(bp) == typeid(Derived))
//该检查永远是失败的
// 一个是指针,永远一个是类对象永远不会相等。
// 此处代码永远不会被执行
// 当typeid作用于指针时,而不是作用于指针所指向的对象
// 返回的指针的静态类型Base*。
// Base*在编译时求值
// ----------表达式是否会求值
// typeid是否需要运行时检查决定了表达式是否会被求值
// 只有当类型含有虚函数时,编译器才会对表达式求值。
// 如果类型不含有虚函数,typeid返回表达式的静态类型,编译器无需对表达式求值就直到表达式的类型
// 如
typeid(*p)
// 如果p所指的类型不含有虚函数,则p不必是一个有效指针,因为不需要对他进行求值;否则p必须是一个有效的指针。
// 如果此时p是一个空指针,将会抛出bad_typeid的异常。(是否是求值时)
}
1).实际应用。
equal
虚函数。equal
只能使用基类的成员。如何解决?{
class Base {
friend bool operator==(const Base&, const Base&);
public:
protected:
virtual bool equal(const Base&) const;
};
class Derived : public Base {
public:
protected:
bool equal(const Base&) const;
};
bool operator==(const Base &l, const Base &r) {
// 先检查typeid的类型是否一致
// 如果不一致,返回false
// 如果一致继续虚调用equal
return typeid(r) == typeid(l) && l.equal(r);
}
//设计equal时需要使用到类型的转换
// dynamic_cast
// 由于需要比较派生类中的成员
// 所以转换是必然的。
bool Derived::equal(const Base &r) const {
// 此时两个类型必然是一样的
// r必然是Derived,所以
// 转换不会发生问题
auto r = dynamic_cast<Derived&>(r);
// .....后续的相等判断操作
}
// 基类中的虚函数,可以之接比较
bool Base::equal(const Base &r) {
//....相等的判断操作
}
}
1).type_info
的精确定义随着编译器的不同略有差异。但是c++规定type_info
类的定义必须在头文件typeinfo
中,并且至少提供表19.1所示的操作。(p735)
type_info
一般是作为一个基类出现,所以他还提供一个public virtual
的析构函数。当编译器希望提供额外的类型信息时,通常在type_info
的派生类中完成。type_info
没有默认的构造函数,而且它的拷贝,移动**都是删除的。**所以我们无法定义该类型的对象。创建type_info
对象的唯一方法就是使用typeid
运算符。typeid
的name
成员返回一个C风格字符串,表示对象的类型名字。对于某一种给定的类型来说,name
的返回值因为编译器的不同而不同,并且不一定与程序中使用的名字一致。对于name
返回值的唯一要求就是,类型不同则返回的字符串必须有所区别。{
int arr[10];
Derived d;
Base *p = &d;
typeid(42).name();//i
typeid(arr).name();//A10_i
typeid(Sales_data).name();//10Sales_data
typeid(string).name();//Ss
typeid(p).name();//P4Base
typeid(*p).name();//7Derived
// type_info在有的编译器上提供了额外的成员函数来提供程序中所用类型的额外信息。
// 详见编译器的使用手册
}
练习
ra
是一个动态类型,需要求值,答案有错误,name
返回的结果应该表示的是表示class B
的字符串。1).它使得我们可以将一组 整型常量 组织在一起。和类一样,枚举类型定义了一种新的类型。它属于字面值常量类型。
2).c++中有两种枚举,
{
// ---------定义限定
enum class open_modes {input, output, append};
enum struct open_modes {input, output, append};
// 首先是关键字,enum class 或者
// enum struct
// 然后是类型名称以及花括号括起来的以逗号分割的 枚举成员列表
// 最后是一个分号
//----------定义不限定
// 省略掉关键字class 或者struct
// 并且枚举类型的名字是可选的
enum color {red, yellow, green};
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
// 如果enum是未命名的,则我们只能在定义该enum的时候定义它的对象
// 方式就是
enum {red, green} aEnum, anotherEnum;
// ----------枚举成员
// 在限定作用域的枚举类型中,枚举成员的名字遵守常规的作用域准则,并且在枚举类型的作用域之外是不可访问的
// 而在不限定作用域的枚举类型中,枚举成员的作用域和枚举类型本身的作用域相同
enum color {red, yellow, green};//不限定
enum stoplight {red, yellow, green};//错误,重复定义了枚举成员
enum class peppers {red, yellow, green};//正确,内部的隐藏外部
color eyes = green;//正确,不限定作用域的枚举类型成员位于有效的作用域中
peppers p = green;//错误,peppers的枚举成员不在有效的作用域中
// 但是color::green在有效的作用域中,但是类型错误
color hair = color::red;//正确,显式访问
peppers p2 = peppers::red;//正确
// --------默认值
// 默认情况下,枚举值从0开始,依次加一
// 不过我们可以为一个或者几个枚举成员指定专门的值
enum class intTypes {
charType = 8, shortType = 16, intType = 16,
longType = 32, long_longType = 64
};
// 枚举值不一定唯一,
// 如果我们没有显式地指定初始值,则当前的枚举类型成员的值,等于之前枚举成员的值加一
//-----------const属性
// 枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式
// 也就是说,每一个枚举类型长远本身就是一条常量表达式,我们可以在任何需要使用常量表达式的地方,使用枚举类型成员。
// -------应用
// 定义枚举类型的constexpr变量
constexpr intTypes charbits = intTypes::charType;
// switch语句
// 将一个enum作为switch语句的表达式
// 枚举值作为case的标签
// 将枚举类型作为一个非类型模板实参使用(例如数组的大小。)
// 因为推断出来的值必须是一个常量表达式。从而允许编译器在编译时实例化模板。
// 在类的定义中初始化枚举类型的静态数据成员
// 在类内提供初始值,要求是初始值是常量表达式,变量是一个constexpr
// 类外初始化没有要求。
// ----------定义和初始化enum类型的对象
// 只要enum有名字,我们就可以定义它并进行初始化
// 要想初始化enum对象或者为它赋值
// 必须使用给类型的一个枚举成员或者该类型的另一个对象
open_modes om = 2;//错误,2不属于该类型
om = open_modes::input;//正确,input是它的一个成员
// 一个 不限定作用域 的枚举类型的对象或者枚举成员自动转换成整型
int i = color::red;//隐式转换
int j = peppers::red;//错误,限定作用域的不会进行隐式转换
// -------------指定enum的大小
// 实际上enum是统一由 某一种整数类型 表示的
// 在C++11中,我们可以在enum名字后面加上冒号以及我们想要使用的类型
// enum intValues : unsigned long long {
charType = 255, shortType = 65535,
longType = 4294967295Ul,
long_longType = 18446744073709551615ULL
};
// 如果我们没有指定enum的潜在类型,则默认情况下
// 限定作用域的enum成员类型是int
// 不限定作用域的枚举类型来说,枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能容纳枚举值。
// 如果我们指定了枚举成员的潜在类型 (包括限定作用域的enum的隐式指定),则一旦某一个成员超过该类型所能容纳的范围,将会引发错误
// 指定enum的潜在类型,使得我们可以控制不同实现环境中使用的类型
// 确保在不同环境中编译所产生的代码一样。
// ----------前置声明
enum intValues : unsigned long long;//不限定作用域的必须指定成员类型
enum class open_modes;//限定的可以使用默认的成员类型int
// 前置声明enum,必须指定成员的类型。可以是显式也可以是隐式。
// 和其他声明一样,enum的声明和定义必须匹配
// 这意味着,enum的所有声明和定义中的成员类型都必须一致
// 而且,我们不能在同一个上下文中,先声明一个不限定作用域的enum
// 然后又声明一个同名的限定作用域的enum
enum class intValues;//使用默认的int
enum intValues;//错误
enum intValues : long;//错误
// --------------形参匹配和枚举类型
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;
}
// 注意不限定的枚举可以向整型的转换
// 只能用枚举成员或者对象进行初始化或者赋值
// 至于枚举向整型的转换,有枚举的潜在类型决定
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); //调用int
newf(uc); //unsigned char
// 这是因为,Tokens的最大成员就是129,该枚举类型可以用unsigned char来表示
// 因此很多编译器使用unsigned char作为enum的潜在类型
// 但是,它的对象和枚举成员此时会提成成int
// 所以精确匹配int的重载版本
}
1).成员指针,指的是可以指向类的非静态成员的指针。一般而言,指针指向的是类的对象,但是成员指针指向的是类的一个成员而不是一个对象。
{
class Screen {
public:
typedef std::string::size_type pos;
char get_char_cursor() const {
return contents[cursor];
}
char get() const;
char get(pos ht, pos, wd) const;
private:
str::string contents;
pos cursor;
pos height, width;
};
}
1).和普通指针声明的区别在于,
{
// 需要包含成员所属的类
// 在声明的*之前添加classname::以表示当前定义的指针可以直指向classname的成员
const string Screen::*pdata;
// 指向Screen类的const string成员的指针
// 常量对象的数据成员本身也是常量,因此我们的指针声明成指向const string成员的指针pdata可以指向
// 任何Screen对象的一个成员,不管该对象是不是一个常量
// 此时该指针只能读,而不能写。
// ----------初始化与赋值
pdata = &Screen::contents;
// 当我们初始化一个成员指针(或者向他赋值)时,需要指定它所指的成员
// 而对象是非特定的。
// 以上表示,指针指向一个非特定的Screen对象的特定contents成员
// 其中,我们将取地址运算符作用于,Screen类的成员而非内存中一个该类的对象
// 在c++11中,声明成员指针最简单的方式就是使用auto或者decltype
auto pdata = &Screen::contents;
// ------------使用数据成员指针
// 当我们初始或者赋值一个成员指针时,该指针并没有指向任何的数据
// 指针指定了成员但是没有指定对象
// 只有当解引用成员指针时我们才提供对象信息。
// 该指针也有两种成员访问运算符
// .*
// ->*
// 我们可以解引用指针并获得该对象的成员
Screen myScreen, *pScreen = &maScreen;
// 解引用pdata以获得myScreen对象的contents成员
auto s = myScreen.*pdata;
// 解引用获得pScreen所指的对象的contents成员
s = pScreen->*pdata;
// 以上的操作经过两个步骤
// 1. 解引用成员指针获得所需要的成员
// 2. 然后通过成员访问运算符得到指定对象的指定成员
// ------------返回数据成员指针的函数
// 常规的类的访问规则对于成员指针同样有效
// contents是私有的
// 对于pdata的使用必须位于Screen类的成员或者友元内部,否则报错
//因为数据成员一般是私有的,所以我们通常不能直接获得数据成员指针
// 如果一个类希望我们访问它的数据成员
// 最好定义一个函数
class Screen {
public:
// data是一个静态成员
static const std::string Screen::* data() {
return &Screen::contents;
}
};
// 得到一个成员指针,此时只是得到一个指针,并没有实际对象
const string Screen:: *pdata = Screen::data();
// 获得myScreen的contents成员
auto s = myScreen.*pdata;
}
1).
{
// 最简单的方式就是使用一个auto关键字
auto pmf = &Screen::get_cursor;
// pmf是一个指针,它可以指向Screen的某一个常量成员函数
// 前提是该函数不接受任何实参,并且返回一个char
// 即该指针指向的函数,必须和get_cursor的返回类型,形参类型,是否const,是否是引用,一致即可
// 当我们不使用auto关键字时
// 需要指定返回类型,形参类型,const属性,引用属性
char (Screen:: *pmf2)(Screen::pos, Screen::pos) const;
pfm2 = &Screen::get;//指向以上函数类型的get。
// 并且成员函数有重载的情况,我们必须显式地指出函数类型,明确我们所要使用的函数是哪一个。即此时,不能使用auto关键字。
//----------优先级
char Screen:: *p(Screen::pos, Screen::pos) const;
// 试图声明一个函数,返回指向Screen类的char数据的指针。
// 由于是普通函数,不可以是const,故报错。
// 和普通指针不一样的是,在成员函数和成员函数指针之间不存在自动转换的规则。
pmf = &Screen::get; //正确,需要显式指出&运算符。
pmf = Screen::get; //错误
// -------------使用成员函数指针
// 和使用解引用运算符
// .*
// ->*
// 进行调用函数
Screen myScreen, *pScreen = &myScreen;
// 注意使用调用运算符()
char c1 = (pScreen->*pmf)();
char c2 = (myScreen.*pmf2)(0, 0);
// 之所以需要(),就是因为调用运算符的优先级比解引用运算符更高。
myScreen.*pmf();
// 等价于
myScreen.*(pmf());
// 因为调用运算符的优先级别高
// 所以在声明和使用时,()都必不可少
(C::*pf)(parms);
(c.*pf)(args);
// -----------使用成员指针的类型别名
// 这样作易于理解
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
// 从而定义一个成员函数的指针变得简单
Action get = &Screen::get;//指向的时Screen的get成员
// ------------函数成员指针作为某一个函数的返回值或者形参类型
// 形参可以默认实参
Screen& action(Screen &, Action = &Screen::get);
// 调用
Screen myScreen;
action(myScreen);
action(myScreen, get);
action(myScreen, &Screen::get);
// 类型别名使得,成员函数指针类型更见容易理解,更加简洁
// ----------成员函数指针表
// 类似于之前定义map,存放同一类型的可调用对象
// string作为key,可调用对象作为value
// 利用标准库的function类进行存放
// 这里,我们使用成员函数指针实现一样的功能。
// 将指针存入一个函数表中。
// 如果一个类含有几个相同类型的函数成员,则这样的表可以帮助我们从这些成员中选择一个
// 例如,以下的成员函数的可调用类型都一致
// 返回类型,形参,const属性,引用属性
class Screen {
public:
// 每一个函数负责光标的移动
Screen& home();
Screen& forward();
Screen& back();
Screen& up();
Screen& down();
};
// 这几个函数的类型一样
// 我们希望定义一个move函数
class Screen {
public:
// 定义一个成员函数指针
using Action = Screen&(Screen::*)();
// 指定具体要移动的方向
enum Directions {HOME, FORWARD, BACK, UP, DOWN};
Screen& move(Directions);
private:
static Action Menu[];//函数表
};
// 数组存放的是每一函数的指针。
// move接受一个对应的枚举类型
// 进行操作
Screen& Screen::move(Directions cm) {
// 典型错误,注意是成员函数指针
// 还没有指定对象。
return (Menu[cm])();
// 调用的是this对象的成员函数
return (this->*Menu[cm])();
}
// -------使用就是
Screen myScreen;
myScreen.move(Direction::HOME);//调用myScreen.home()
myScreen.move(Direction::DOWN);//调用myScreen.down()
// --------初始化函数表
// static的类外初始化。
Screen::Action Screen::Menu[] = {
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down
};
}
练习,
1).成员函数指针本身并不是一个可调用对象。所以它并不能直接转递给一个算法。
function
标准库模板,生成一个可调用对象。{
function<boo (const string &)> fcn = &string::empty;
find_if(sv.begin(), sv.end(), fcn);
// ~~通常情况下,执行成员函数的对象被隐式地传递给this形参,当我们想要使用function为成员函数生成一个可调用对象时,必须按照一样的方式,但是不需要我们操作。...p745.~~
// 当一个function对象包含一个指向成员函数的指针时,function类知道它必须使用正确的指向成员的指针运算符来执行函数的调用。
// 也就是说,我们可以认为在find_if当中含有类似于如下形式的代码
if (fcn(*it))
// it是find_if实参范围内的迭代器,*it是给定范围的一个对象
// fcn是传递给find_if的function类型的可调用的对象
// 其中function将正确使用指向成员函数的指针
// 本质上,function类将调用转换成了如下的形式
// it是迭代器
if((*it).*p)())//p是一个fcn内部的成员函数指针
// 我们必须根据所需要的类型来指定function的对象中包含的可调用对象
vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
find_if(pvec.begin(), pvec.end(), fp);
vector<string> sv;
function<bool (const string&)> fp1 = &string::empty;
find_if(sv.begin(), sv.end(), fp1);
}
mem_fn
生成一个可调用对象{
// 使用function必须由用户显式地提供可调用对象的类型,
// 使用标准库功能mem_fn,让编译器负责推断可调用对象的类型,无需用户显式指定。
// 它和function都定义在头文件functional中,并且可以从成员指针生成一个可调用对象
find_if(sv.begin(), sv.end(), mem_fn(&string::empty));
// 使用mem_fn(&string::empty)生成一个可调用对象,该对象接受一个string实参,返回一个bool值
// ---------调用形式
auto f = mem_fn(&string::empty);//f接受一个string或者一个string*
f(*sv.begin());//传入的是一个string,f使用.*调用empty
f(&sv.begin());//传入的是一个string*,f使用->*调用empty
// 实际上,我们可以认为mem_fn生成的可调用对象有一对重载的函数调用运算符,一个接受指针,一个接受对象
}
bind
生成一个可调用对象{
auto it = find_if(sv.begin(), sv.end(), bind(&string::empty, _1));
// 与function类似的地方是,当我们使用bind时,必须将函数中表示执行对象的隐式形参(this)转换为显式?
// 如何转换?迷惑
// 和mem_fn类似的是,既可以接受指针,也可以接受一个对象
auto f = bind(&string::empty, _1);
f(*sv.begin());//string,f使用.*调用empty
f(sv.begin());//string*,f使用->*调用empty
// 具体如何是实现?什么叫做,将调用对象的隐式形参(this)转为显式?
}
练习,
string::empty
。1).一个类可以定义在另一个类的内部,前者称为嵌套类或者嵌套类型。
QueryResult
类line_no
的再一次定义。)。2).为什么要定义嵌套类?
public
部分的嵌套类实际上定义了一种可以随机访问的类型;定义在protected
部分的嵌套类定义的类型,只能被外层类以及它的友元,派生类所访问;定义在外层类的private
部分的嵌套类定义的类型,只能被外层类以及它的友元所访问。3).声明一个嵌套类
{
// -------------声明
// TextQuery和QueryResult类密切相关
// QueryResult主要是作为TextQuery中函数query的结果
// 用作其他没有意义
// 我们可以将QueryResult定义成TextQuery的成员
class TextQuery {
public:
class QueryResult;//稍后定义
};
// 由于,将QueryResult类声明为嵌套类,所以我们必须先声明在进行使用
// 因为后面query需要它作为返回类型
// ----------类外定义一个嵌套类
// 和成员函数一样,嵌套类必须在类内声明
// 但是定义可以在类外部,也可以在类内部
// 需要前缀
class TextQuery::QueryResult {
// 此时是位于TextQuery类内
// 不需要对QueryResult的形参进行限定
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
// 嵌套类可以直接使用外层类的成员,无需对该成员进行名字的限定
// 但是前提是,外层类设置了友元,
// 因为嵌套类对外层类的没有特殊的访问权限。
};
// 嵌套类在外层类之外定义完成之前,它都是一个不完全类型
// ----------定义嵌套类的成员
// 唯一的不同就是指明嵌套关系
// 用外层类限定内层嵌套类
// 以及内层类在是外层友元的前提下,可以直接使用外层类的成员,而不需要进行限定
TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p,shared_ptr<vector<string>> f) :
sought(s), lines(p), file(f) {}
// -------------嵌套类的静态成员定义
// static声明只能在类内
int TextQuery::QueryResult::static_mem = 1024;
// 在嵌套类内声明的静态成员
// 静态成员的定义将位于TextQuery作用域之外
//---------嵌套类作用域的名字查找
// 嵌套类的名字查找遵循一般的规则
// 注意它是一个嵌套的作用域
// 但是同时还需要注意,嵌套的类对外层类没有特殊的访问权限
// 反过来,嵌套类作为外层类的一个类型成员,可以被外层类的成员直接使用(和其他的成员没有什么区别)。
// 需要注意的是,在类外部定义成员函数时,返回类型是没有进入类内的。!
// 所以还是需要类型限定符号。
}
1).它是一种特殊的类。一个union
可以有很多的数据成员,但是在任意时刻只有一个数据成员可以有值。
union
的某一个成员赋值之后,该union
的其他成员就变成未定义的状态。分配给一个union
对象的存储空间至少要容纳它的最大数据成员。union
定义了一种新类型。2).类的某些特性对于union
同样使用。
union
不能含有引用类型。其他的类型大多可以作为它的成员类型。union
的成员类型。union
可以为其成员指定public,protected,private
等多种保护标记。默认情况下,union
是public
。union
可以定义包括构造函数和析构函数在内的成员函数。但是,union
不可以作为基类和也不能继承自其他类,所以union
中不能含有虚函数。2).union
{
// union提供了一种有效的方式,使得我们可以方便地表示一组类型不同的互斥值
// -------------定义一个union
union Token {
char cval;
int ival;
double dval;
};
// 我们需要处理一组不同类型的数据
// Token类型的对象,只有一个成员,该成员的可能是以上的任意一个
// 注意,union后的类型名称是可选的
// -------------使用一个union
// 注意union的名字就是一个类型名。
// 通常情况下,union的对象是没有初始化的
// 我们可以使用花括号来显式初始化一个union
Token first_token = {'a'};//初始化cval成员
Token last_token;//未初始化的Token成员
Token *pt = new Token;//指向一个未初始化的Token对象
// 如果提供了初始值,则该初始值被用于初始化 第一个 成员
// -----------使用成员访问运算符,,为其他的指定成员赋值
last_token.cval = 'z';
pt->ival = 42;
//-------------注意
// 为一个union的一个 数据 成员赋值时,会使得其他的数据成员处于未定义的状态
// 因此当我们使用union时,必须清楚地知道,当前的union存储的数据到底是什么类型。
// 如果我们使用错误的数据成员或者为错误的数据成员赋值
// 程序可能会崩溃或者出现异常的行为,具体的视情况而定。
// -------------匿名的union
// 指的就是一个未命名的union
// 并且在右括号和分号之间没有任何的对象声明。
// 注意,一旦,我们定义了一个匿名的union,编译器就会自动地为该union创建一个未命名的对象。
union {
char cval;
int ival;
double dval;
}; //定义一个未命名的对象,我们可以直接访问它的成员
// 在匿名union的定义所在的作用域中,该union的成员都是可以直接访问的。 如何使用?
// 匿名union不能包含protected,private成员,也不能定义成员函数。
// ------------含有类类型成员的union
// 早期中,不允许有定义了构造,或者拷贝控制成员类类型成员
// 新标准中允许。但是,如果union里的成员类型定义了,自己的构造或者拷贝控制成员,则该union的用法要比只含有内置类相关的成员的union更加复杂
// 当union包含的是内置类型的成员时,我们可以使用普通的赋值语句改变union保存的值
// 但是对于含有特殊类类型成员的union时,就没有那么简单了
// 如果我们想要将union的值改为类类型成员对应的值时,必须构造该类型的对象
// 同理,如果我们想要将一个类类型的值改为一个其他的值,就必须析构该类类型的成员。
// 当只有内置类型成员时,union的析构和构造由编译器完成
// 当是类类型时,并且自定义了构造函数和拷贝控制成员,编译器的合成默认版本是删除的
// 如果在一个类中含有一个union成员,并且该union成员含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作将是删除的
// ---------------------使用类管理union成员
// 管理含有类类型成员的union
class Token {
public:
// 因为union定义了一个string成员,所以该Tokne必须定义拷贝控制成员
// 默认的版本是删除的。??
Token() : tok(int), ival(0) {} //默认构造函数
// 不接受参数就是一个默认构造函数。
Token(const Token &t) : tok(t.tok) {
copyUnion(t);
}
Token& operator=(const Token &t);
~Token() {
// 必须显式地析构string成员
if (tok == STR) sval.~string();
}
// 完成对应的赋值。
// 控制类型的转换。
Token& operator=(const string&);
Token& operator=(char);
Token& operator=(int);
Token& operator=(double);
private:
// 追踪union中存放的值的类型
enum {INT, CHAR, DBL, STR} tok;//判别式
union { //匿名的union
char cval;
int ival;
double dval;
std::string sval;
};
// 每一个Token对象含有一个该未命名的union类型的一个未命名成员
// 它是可以直接使用的
void copyUnion(const Token&);
};
// 因为我们的union含有一个定义了析构函数的成员
// 所以我们必须为union定义一个析构析构函数以销毁string成员。和普通的类类型成员不一样,作为union组成部分的类成员无法自动销毁
// 因为析构函数不清楚union存放的值是什么类型,所以它无法确定应该销毁哪一个成员
// 我们的类析构函数会进行检查,如果union里面存放的是内置类型,则类的析构析构函数什么也不做。
// -----------------重载赋值运算符
Token& Token::operator=(int i) {
if (tok == STR) sval.~string();
ival = i;
tok = INT;
return *this;
}
// ----------------string版本的,略有不同
Token& Token::operator=(const string &s) {
if (tok == STR)
sval = s;//如果是string,直接赋值即可
else
// 该定位new没有申请实参,而是赋值作用
new(&sval) string(s);//否则是直接构造一个
// 这里是使用了一个定位new
tok = STR;
return *this;
}
// ----------------管理需要拷贝控制的联合成员
// 分清楚构造和赋值的不同
// 初始化,对象是一个没有存放值得
// 赋值,对象又可能有,又可能没有存放值
void Token::copyUnion(const Token &t) {
switch(t.tok) {
case INT : ival = t.ival; break;
case CHAR : cval = ...
// 要拷贝一个string,可以使用定位new表达式来构造它
case string : new(&sval) string(t.val); break;
}
}
Token& Token::operator=(const Token&t) {
// 针对原来union和实参得union的值类型
// 做出三种的判断。
if (tok == STR && t.tok != STR) sval.~string();
if (tok == STR && t.tok == STR)
sval = t.sval;//无需构造一个新的string
else //这里需要注意,我们的第一种情况也是执行以下的函数
copyUnion(t);
tok = t.tok; //更新类型标志。
return *this;
}
}
1).类可以定义在某一个函数里面,这样的类是局部类。局部类定义的类型,只在定义它的作用域内可见。和嵌套类不一样,局部类的成员受到严格限制。
2).局部类不能使用函数作用域中的变量
{
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) {
barVal = val; //错误,val是foo的一个局部变量,形参
barVal = ::val; //正确,使用一个全局对象
barVal = si; //正确,使用一个静态局部对象
barVal = b; //正确,使用一个枚举成员
}
};
}
}
3).常规的访问保护规则对局部类同样适用
public
。在程序中有权力访问局部类的代码非常有限。局部类已经封装在函数作用域里面,通过信息隐藏进一步封装就显得没有什么必要。4).局部类的名字查找
5).嵌套的局部类
{
// 可以在局部类中,嵌套一个类
// 此时,嵌套类的定义可以出现在局部类之外,但是
// 嵌套类的定义必须在与局部类相同的作用域中。
// 即只能在函数中
void foo() {
class Bar {
public:
class Nested;//声明一个类
};
// 定义
class Bar::Nested {
...
};
}
// 局部类的嵌套类,也是一个局部类,必须遵守局部类的各种规定。
// 嵌套类的所有成员必须定义在嵌套类内部。
}
1).为了支持底层编程。c++定义了一些固有的不可移植的特性。所谓的不可移植就是因机器而异的特性。
1).类可以将它的(非静态)数据成员定义成bit-field
,在一个位域中含有一定数量的二进制位。
{
// 位域的声明就是成员名字后买你紧跟一个冒号和一个常量表达式,用来指定成员所占的二进制位数。
typedef unsigned int Bit;
class File {
Bit mode: 2;//mode占2位,两个二进制位
Bit modified: 1;//1
Bit prot_owner: 3;
Bit prot_group: 3;
Bit prot_world: 3;
// 操作和其他数据成员
public:
// 文件类型以八进制的形式表示
enum modes {READ = 01, WRITE, EXECUTE};
File &open(modes);
void close();
void write();
void isRead() const;
void setWrite();
};
// 如果可能的话
// 尽量,在类的内部连续定义位域,以便压缩在同一个整数的相邻位,从而提供存储压缩
// 例如,这5个位域可能会存储在同一个unsigned int 中。
// 这些二进制位是否能压缩到一个整数以及如何实现,是与机器相关的。
// &,不能作用于位域,因此任何指针都不能指向类的位域。
// ---------------使用位域
// 访问位域的形式和访问类的其他成员的形式很相似
void File::write() {
modified = 1;
// ...
}
void File::close() {
if (modified)
// ...
// 保存内容
}
// 通常使用内置的位运算符,操作超过1位的位域
File& File::open(File::modes m) {
mode |= READ;//设置为READ
// 其他处理
// ?
if (m & WRITE) //如果打开了READ和WRITE
// 按读写方式打开文件
return *this;
}
// 如果一个类设置了位域,通常也会定义一系列inline的操作来检验和设置位域的值
inline bool File::isRead() {return mode & READ;}
inline void File::setWrite() {mode |= WRITE;}
}
1).它的确切含义和机器有关。只能通过阅编译器文档来理解。要想使用volatile
的程序在移植到新机器或者新编译器后,仍然有效,通常需要对程序做一些改变。
2).直接处理 硬件 的程序常常包含这样的元素,它们的值由程序直接控制之外的过程控制。
volatile
。volatile
告诉编译器,不应对这样的对象进行优化。{
// 用法和const很相似
volatile int display_register;//该int值可能发生改变
volatile Task *curr_task;//指向一个volatile对象
volatile int iax[max_size];//iax中的每一个元素都是volatile
volatile Screen bitmapBuf;//每一个成员都是volatile
// 某一种类型既可以是const,也可是volatile
// 也可以同时具有两种属性,
// 这两个限定符号相互之间没有什么影响。
// 就想一个类可以定义const成员函数一样,他也可以定义volatile的成员函数,此时只有volatile的成员函数,能被volatile对象调用。
//------------指针和volatile
// 这个关系和const一致
volatile int v;
int *ip = &v;//错误,必须使用volatile指针
volatile int *ivp = &v;//正确
volatile int volatile *vivp = &v;//正确
// ----------------合成的拷贝对volatile对象无效
// const和volatile的一个重要区别是,我们不能使用合成的拷贝/移动构造函数以及赋值操作初始化volatile对象
// 或者从volatile对象赋值
// 合成的成员接受的形参是非volatile的
// 解决就是自己定义相应的函数
class Foo {
Foo(const volatile Foo &);
// 赋值给非volatile对象
Foo& operator=(volatile const Foo&);
// 赋值给volatile对象
Foo& operator=(volatile const Foo&) volatile;
};
// 拷贝一个volatile对象是否有意义呢?
// 与使用目的相关。??
}
1).C++程序有时候需要调用其他语言编写的函数,最常见的就是调用C语言编写的函数。
2).声明一个非C++的函数
{
// 链接指示可以有两种形式,
// 1. 单个
// 2. 复合
// 链接指示不能出现在类定义或者函数定义的内部。同样的链接指示必须在的函数的每一个声明中出现
// cstring头文件中的某一些c函数是如何声明的
// 单个语句
extern "C" size_t strlen(const char *);
// 复合语句
extern "C" {
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
// 链接指示就是一个extern关键字,后面是一个字符串字面量
// 然后就是一个普通的函数声明。
// 编译器应该支持对C语言的链接指示
// 此外,编译器也可能会支持其他语言的连接指示
extern "Ada"
extern "FORTRAN"
}
3).链接指示和头文件
{
// 复合形式的链接指示
// 加上了花括号
// 作用在于,
// 1. 一次性声明若干函数,建立多个链接。
// 2. 将使用于该来链接指示的多个声明聚在一起
// 花括号中的声明的函数名字是可见的,就好像在花括号之外声明一样的。
// 还可以应用于整个头文件
// 例如,cstring头文件可能形如。
extern "C" {
#include
}
// 当一个include指示被至于复合链接指示的花括号中时,
// 头文件中的所有普通函数声明都被认为是来链接指示语言所编写的
// 链接指示可以嵌套,因此如果头文件包含自带来凝结指示的函数
// 则该函数的链接不受影响
// C++从c语言继承的标准库可以定义成c函数
// 但并非必须,决定使用c还是c++实现c标准库,是每一个c++实现事情。
}
4).指向extern "C"函数的指针
{
// 编写函数所用的语言是函数类型的一部分,因此,对于使用链接指示定义的函数来说,
// 它的每一个声明都是需要使用相同的链接指示
// 而且指向其他语言编写的函数的指针必须和函数本身使用一样的链接指示
// pf指向一个c函数,该函数接受一个int返回void
extern "C" void (*pf)(int);
// 当我们使用pf调用函数时,编译器认定当前调用的是一个C函数。
// 指向c函数的指针和指向c++函数的指针是不一样的类型。
// 这就是类型不匹配的问题。
void (*pf1)(int);
extern "C" void (*pf2)(int);
pf1 = pf2; //错误。
pf2 = pf1; //错误。
// 有的编译器会接受第一个赋值语句,并把它作为对语言的扩展
// ----------------链接指示对整个声明都有效
extern "C" void f1(void (*)(int));
// f1是一个c函数,它的形参是一个指向c函数的指针
// extern "c"不仅对于函数有效。
// 对于它的返回类型,形参的函数指针类型一样有效。
// 如果我们希望给c++传入一个指向c函数的指针
// 使用类型别名
extern "C" typedef void fc(int);
// f2是一个c++函数,该函数的形参是指向c函数的指针
void f2(fc *);
// -------------------导出c++函数到其他语言
// 使用链接指示,对函数 定义 ,我们可以另一个c++函数在其他语言编写的程序中可用。
// f函数可以被c程序调用
extern "C" double f(double d) {/*.....*/}
// 编译器为该函数生成合适的指定语言的代码
// 可被多种语言共享的函数的返回类型和形参类型受到很多的限制
// 例如,我们不太可能把一个C++类的对象传给c程序,因为c程序根本无法理解构造函数,析构函数
// 以及类特有的操作
// ----------------预处理器??
// 有时需要在c和c++中编译同一个源文件
// 我们可以,在编译c++版本的程序时,预处理器定义_ _cpluspluls。
// 利用这个变量,我们可以在编译c++程序的时候有调剂爱你地包含一些代码进来
#ifndf __cplusplus
// ture,我们正在编译c++程序
extern "C"
#endif
int strcmp(const char*, const char*);
// ---------------重载函数和链接指示
// 链接指示和重载函数的相互作用依赖于目标语言(例如,"C")
// 如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也是支持重载c++中的函数。(->c++)
// c语言不支持重载,所以一个c链接指针只能用于一个重名的函数。
// 错误,两个extern "C"函数的名字相同
// c语言中不可能会同时有这两个函数。
extern "C" void print(int);
extern "C" void print(double);
// 如果一组重载函数中有一个是c函数
// 其余必定是c++函数???
class SmallInt {...};
class BigINt {...};
// c函数可以在c++中调用
extern "C" double f(double);
// c++中的重载函数组
extern SmallInt f(const SmallInt &);
extern BigNum f(const BigNum &);
// ...为什么加上extern关键字
}
1).位域和volatile
,使得程序容易访问硬件。链接指示使得程序易于访问其他语言编写的函数。