某些应用程序对内存分配有特殊需求,如使用new将对象放置在特定的内存空间中,为实现它,应用程序需要重载new和delete。
new实际执行步骤:
1.new表达式调用operator new(或operator new[])的标准库函数,分批一块足够大的、原始的、未命名的内存空间以便存储特定类型对象(或对象的数组)。
2.编译器运行构造函数构造对象。
3.对象被分配了空间并构造完成,返回指向它的指针。
delete执行步骤:
1.对指针指向的对象(数组中每个对象)调用析构函数。
2.编译器调用operator delete(operator delete[])回收空间。
编译器会使用我们自定义的new或delete版本替换标准库版本。
我们可以将new或delete定义在全局作用域或定义为成员函数,在使用它们时,如被分配或释放的对象是类类型,则编译器首先去该类及其基类中查找,如没找到,再到全局作用域中找自定义的版本,如还没找到,最后再用标准库定义的版本。我们可以用作用域运算符指定查找作用域,如不想使用类中定义的版本,可以用::new或::delete。
标准库有8个delete和new函数:
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_t &) noexcept;
void operator delete(void *,nothrow_t &) noexcept;
void operator delete[](void *, nothrow_t &) noexcept;
nothrow_t是定义在头文件new中的一个struct,这个类型中不包含任何成员。头文件new中还含有一个名为nothrow的const对象,可以用它请求new的非抛出版本。与析构函数类似,delete函数不允许抛出异常。书上说重载不抛出异常的new或delete运算符时,必须指定noexcept说明其不抛出异常,但我测试时不指定也能运行,但最好还是加上:
void operator delete[](void *p, nothrow_t&) { } // 可通过编译
我们可以自定义任意一个版本,但必须在类作用域或全局作用域中定义,如我们定义在类作用域中,它是隐式static的函数且不能操纵类的成员,因为new用在对象构造之前,delete用在对象销毁之后。
new的返回类型必须是void *且第一个形参必须是size_t且不能有默认实参。编译器调用new时,把存储指定类型对象所需的字节数传给size_t;调用new[]时,传入的是数组中所有元素所需的空间。
自定义的new可以提供额外的形参,此时,需要定位new才能调用它。
以下函数不能被用户重载,它只供标准库使用:
void *operator new(size_t, void *);
函数delete和delete[]的返回类型必须是void且第一个形参类型必须是void *,执行它时,用指向待释放内存的指针来初始化void *形参。
函数delete或delete[]定义成类的成员时,它可以包含另一个size_t类型的形参,表示第一个形参所指对象的字节数,可用于删除继承体系中的对象。如果基类中有一个虚析构函数,则传入的字节数由第一个参数的指针指向对象的动态类型决定。而且运行的delete函数版本由对象的动态类型决定。
标准库函数operator new和operator delete与其他operator函数不同,它们没有改变new或delete表达式的行为,它们的行为永远是内存分配,只不过方式有所区别。我们不能改变new或delete运算符的基本含义。
C++从C继承了malloc和free函数,它们定义在cstdlib头文件中。malloc接受一个表示待分配字节数的size_t,返回指向分配空间的指针(成功)或0(失败)。free接受一个void *参数,它是malloc返回的指针的副本,free将相关内存返回给系统,free(0)没有意义。
一种编写operator new和operator delete的方式:
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);
}
operator new和operator delete一般用于new和delete表达式,但它们也是函数,我们可以直接调用它。
C++ 11前,allocator类还不是标准库一部分,如想把内存分配和初始化分离开,需要operator new和operator delete函数,它们的行为与allocator的allocate成员和deallocate成员很类似,它们分配或释放空间,但不构造或销毁对象。operator new分配的空间我们无法使用allocator的construct成员构造对象。我们可以使用定位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 *)
我们不能重定义以上函数,否则会提示已有此函数,该函数不分配任何内存,它只返回指针实参,然后new表达式在指定地址初始化对象。即传入指针的定位new在一个预先分配的内存地址(就不用分配内存了)上构造对象。
我们传给construct的指针必须指向同一个allocator对象分配的空间,但传给定位new的指针无须指向operator new函数分配的内存,甚至不需要指向动态内存。
string *sp = new string("a value");
sp->~string(); // 析构函数显式调用
以上与调用destroy类似,销毁对象但不回收空间,还可以继续用。
运行时类型识别(RTTI)由两个运算符实现:
1.typeid运算符,返回表示表达式类型的类的对象。
2.dynamic_cast运算符,用于安全地在派生类和基类的引用或指针间转换。
这两个运算符用于某种类型的指针或引用,当该类型含虚函数时,运算符将使用该指针或引用所绑定对象的动态类型。
这两个运算符用于:想通过基类对象的指针或引用使用派生类的操作,但该操作不是虚函数。
dynamic_cast使用形式:
dynamic_cast<type *>(e) // 此时e必须为有效的指针
dynamic_cast<type &>(e) // 此时e必须是一个左值
dynamic_cast<type &&>(e) // 此时e不能是左值
type必须是类类型,通常它应该还含有虚函数。
e必须符合以下其中一条:
1.e的类型是type的public派生类。
2.e类型是type的public基类。
3.e的类型必须是type。
如以上一条都不符合,则转换失败,此时,如转换目标是指针类型,返回0;如转换目标是引用类型,则抛出bad_cast异常。
用法:
if (Derived *dp = dynamic_cast<Derived *>(bp)) { // 条件处定义dp,同时完成类型转换和条件检查,且一旦转换失败,外界也不会接触到这个未绑定的空指针
// 使用派生类指针dp
} else {
// 使用基类指针bp
}
如dp实际指向的是派生类Derived对象,则转换成功。
我们对空指针执行dynamic_cast的结果是所需类型的空指针。
由于不存在空引用,向引用的转换的失败报告策略不同,一旦向引用的转换失败,抛出std::bad_cast异常,它定义在头文件typeinfo中,因此,使用引用转换的代码应类似于以下代码:
void f(const Base &b) {
try {
const Derived &d = dynamic_cast<const Derived &>(b);
} catch (bad_cast) {
}
}
class A {
public:
virtual void func() { }
};
class B : public A { };
class C : public B { };
class D : public B, public A { };
int main() {
A* pa = new C;
B* pb = dynamic_cast<B*>(pa);
if (pb == nullptr) {
cout << "转换失败" << endl; // 不输出
}
B* pb = new B;
C* pc = dynamic_cast<C*>(pb);
if (pc == nullptr) {
cout << "转换失败" << endl; // 会输出
}
A *pa = new D;
B* pb = dynamic_cast<B*>(pa);
if (!pb) {
cout << "转换失败" << endl; // 不输出
}
}
typeid获取表达式类型:
typeid(e); // e为任意表达式或类型的名字
typeid返回一个常量对象的引用,该对象类型为type_info或其public派生类类型,type_info定义在头文件typeinfo中。
typeid作用于顶层const时,顶层const被忽略,如表达式是一个引用,则typeid返回该引用所引对象的类型;作用于数组或函数时,并不会执行向指针的标准类型转换,即作用于数组时得到的还是数组类型。运算对象不属于类类型或不包含任何虚函数的类时,typeid指示运算对象的静态类型,而运算对象是定义了虚函数的左值类型时,typeid的结果运行时才求得。
使用typeid:
Derived *dp = new Derived;
Base *bp = dp;
if (typeid(*bp) == typeid(*dp)); // 检查两者类型是否相同,当继承体系中有虚函数时,返回true
if (typeid(*bp) == typeid(Derived)): // 检查bp指向的对象类型,当继承体系中有虚函数时,返回true
如上,运算对象应该是对象而非指针,因此使用的*bp而非bp。当typeid作用于指针时,返回的是指针的静态类型。
对于typeid(*p),如果指针p所指类型不含虚函数,则p不用是一个有效指针(因为不含虚函数时返回的是p所指对象的静态类型,不用运行时识别);否则,p必须是一个有效指针(因为需要运行时识别p所指对象的动态类型),如果运行时p为空指针,则抛出bad_typeid异常。
我们想为具有继承关系的类实现相等运算符时,可用RTTI。对两个对象来说,如果它们类型相同且对应数据成员值相同,则两个对象是相等的。我们可以用虚函数,令其在继承体系各个层次上分别执行相等性判断,但虚函数的基类和派生类版本必须具有相同的形参类型,必须是基类的引用,此时相等性判断虚函数就不能访问基类之外派生类的数据成员了。如果相等性运算符比较两个不同对象,则应返回false。以上问题和需求可用RTTI解决:
class Base {
friend bool operator==(const Base &, const Base &);
protected:
virtual bool equal(const Base &) const;
}
class Derived : public Base {
protected:
bool equal(const Base &) const;
};
bool operator==(const Base &lhs, const Base &rhs){
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs); // equal是虚函数,运算对象lhs是什么类型的就动态调用哪个类型的虚equal函数
}
bool Derived::equal(const Base &rhs) const{ // 形参必须是基类的引用或指针,因为虚函数的形参都要相同
auto r = dynamic_cast<const Derived &>(rhs); // 所有派生类equal第一件事是将基类转换为对应派生类类型,基类的就不用这么做了,这样做是安全的,因为==运算符已经确认了rhs与lhs类型相同
// 执行数据成员比较操作
}
type_info一般作为基类出现,因此它还应提供一个虚析构函数。
type_info类没有默认构造函数,它的拷贝、移动构造函数和赋值运算符都定义为删除的。只能通过typeid运算符返回type_info类对象。
type_info的name方法返回C风格字符串,表示对象的类型名,它的值因编译器而异并且不一定与在程序中使用的名字一致,对name方法返回值的唯一的要求是类型不同,返回的字符串要有所区别。
枚举类型使我们能将一组整型常量组织在一起,枚举类型定义了一种新类型。枚举属于字面值常量类型。
C++包含限定作用域的和不限定作用域的枚举,前者是在C++ 11中新引入的。
定义限定作用域的枚举类型:
enum class open_modes { input, output, append }; // class也可换成struct
定义不限定作用域的枚举类型时省略掉class或struct,且枚举类型名是可选的:
enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10, double_doublePrec = 10 } f; // f类型为该未命名enum,它的取值只能是这三个值之一
如果定义时省略了枚举类型名,则我们只能在定义该enum时定义它的对象。限定作用域的enum必须是命名的。
限定作用域的枚举类型只能通过作用域运算符获取,而当不限定作用域的枚举类型的成员被放在限定作用域的枚举类型的同一作用域中时:
enum color { red, yellow, green };
enum stoplight { red, yellow, green }; // 错误,重复定义了枚举成员
enum class peppers { red, yellow, green }; // 正确,枚举类型成员被隐藏在了作用域peppers中
color eyes = green; // 正确,green在有效作用域中,此处的green是color的枚举成员
peppers p = green; // 错误,peppers的green不在有效作用域中,但color的green在有效作用域中,但枚举类型不匹配
color hair = color::red; // 正确,允许显式地访问枚举成员
peppers p2 = peppers::red; // 正确,使用peppers的red
默认,枚举值从0开始,依次加1,但我们也能指定值:
enum class intTypes { charTyp = 8, shortTyp = 16, intTyp = 16 }; // 枚举值不一定唯一
如没有显式提供初始值,当前枚举成员的值等于之前枚举成员的值加1。
初始化枚举成员的值提供的初始值必须是常量表达式。即,枚举成员本身就是常量表达式。我们可以定义枚举类型的常量表达式:
constexpr intTypes charbits = intTypes::charTyp;
类似地,我们可以将一个enum作为switch语句的表达式,而将枚举值作为case的标签;或将枚举类型作为非类型模板形参使用;或在类的定义中初始化枚举类型的静态常量数据成员(可用于在旧的不支持类内const static int数据成员赋值的编译器上以替换带类内初始值的const static int成员,此成员可用在需要常量表达式的地方,如数组大小等)。
只要enum有名字,就能定义并初始化该类型成员。要想初始化enum对象或为enum对象赋值,必须使用一个枚举成员或该枚举类型的另一个对象。
不限定作用域的枚举类型对象或枚举成员自动转换成int,我们可以在任何需要整型值的地方使用它们:
int i = color::red; // 正确
int j = peppers::red; // 错误,限定作用域的枚举类型不会进行隐式转换
实际,enum是由某种整数类型表示的,C++ 11中。我们可以指定该enum中元素使用的类型(必须是整型):
enum intValues : unsigned long long { charTyp = 255, long_longTyp = 18446744073709551615ULL };
限定作用域的enum成员类型默认是int。不限定作用域的枚举类型不存在枚举成员的默认类型,我们只知道成员的潜在类型足够大,肯定能容纳枚举值。如果我们指定了枚举成员的类型,一旦枚举成员的值超过该类型最大容纳范围,将引发程序错误。
指定enum的类型,可以确保在多种实现环境中编译器通过的程序生成的代码一致。
C++ 11中,我们可以在前置声明enum时指定enum元素的类型:
enum intValues : unsigned long long; // 不限定作用域的,只能显式指定成员类型
enum class open_modes; // 成员类型为隐式int
enum的所有声明和定义中的元素类型必须一致。
不能在同一个上下文中声明两个同名枚举,无论是不是限定作用域的:
enum class intValues;
enum intValues; // 错误,已被声明为限定作用域的枚举
enum intValues : long;
enum intValues; // 报错与之前的声明不一样,以前是long现在是int
enum Tokens { INLINE = 128, VIRTUAL = 129 }; // 不限定作用域的枚举类型,潜在类型因机器而异
void ff(Tokens);
void ff(int);
int main() {
Tokens curTok = INLINE;
ff(128); // 匹配int
ff(INLINE); // 匹配Tokens
ff(curTok); // 匹配Tokens
}
不能将整型值传给enum形参,但可以将一个不限定作用域的枚举类型对象传给整型形参:
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); // 调用int,由于Tokens中最大值为129,很多编译器使用unsigned char作为Tokens的潜在类型,不管Tokens的类型是什么,它的对象和枚举成员都提升为int,即使它类型是unsigned char
newf(uc); // 调用unsigned char
成员指针是可以指向类的非静态成员的指针,一般,指针指向一个对象,但成员指针指向的是类的成员,因此定义时需要指定类名,在指针名前面加上类作用域运算符指明类即可。指向静态成员的指针与普通指针没什么区别,因为静态成员不属于类。
定义成员指针:
const string Screen::*pdata; // pdata可指向Screen对象的string成员
pdata = &Screen::contents; // 初始化或赋值一个成员指针时,需要加作用域限定,pdata可指向Screen类对象的contents成员
// contents成员必须可访问,否则报错,而书上出了小错误,书上的contents成员是private的
auto pdata = &Screen::contents; // C++ 11中的简便写法
初始化或赋值一个成员指针时,并没有指向任何数据,解引用成员指针时才提供对象的信息:
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata; // .*解引用pdata以获得myScreen对象的contents成员
s = pScreen->*pdata; // ->*解引用pdata以获得pScreen所指对象的contents成员
s = (*pScreen).*pdata; // 等价于上句代码
由于contents是私有成员,因此对于pdata的使用必须位于Screen类的成员或友元内部。
数据成员一般是私有的,我们就不能直接获得数据成员的指针,因此最好定义一个返回值是指向该私有数据成员的指针:
class Screen {
public:
static const std::string Screen::*data() {
return &Screen::contents;
}
};
const string Screen::*pdata = Screen::data();
auto s = myScreen.*pdata; // 获取myScreen的pdata成员
如果上述代码返回的指针不是const的,则可以修改此值:
class Screen {
public:
static std::string Screen::* data() {
return &Screen::contents;
}
private:
string contents = "ddssd";
};
int main() {
Screen myScreen;
string Screen::* pdata = Screen::data();
myScreen.*pdata = "ddd"; // 修改myScreen的private数据成员
auto s = myScreen.*pdata;
cout << s << endl; // 输出ddd
}
以上data函数是static的,但可以访问非static的数据成员的地址:
class Screen {
public:
static const string Screen::* pdata() {
return &Screen::contents;
}
void modifyContents(string s) {
contents = s;
}
private:
string contents;
};
int main() {
Screen s;
s.modifyContents("ssssss");
const string Screen::* pdata = Screen::pdata(); // pdata可指向Screen对象的pdata方法的返回值
cout << s.*pdata << endl; // 输出ssssss
cout << Screen::pdata(); // 输出1
}
如果static方法不是访问非static数据成员的地址,而是值,则会报错:
class Screen {
public:
static void func() {
auto a = &Screen::contents; // 正确
auto b = contents; // 错误
auto c = Screen::contents; // 错误
}
private:
string contents;
};
auto pmf = &Screen::get_cursor; // pmf指向Screen的成员函数get_cursor
指向成员函数的指针也需要指定目标函数的返回类型和形参列表,如成员函数是const或引用成员,也要将const限定符和引用限定符包含进来。
char (Screen::*pmf2)(Screen::pos, Screen::pos) const; // 相比普通函数指针,成员函数指针声明时需要在指针名前加上类名和作用域运算符,且要加上函数的const和引用限定符
pmf2 = &Screen::get;
上例的Screen::*pmf2
两端的括号不能省略,如省略了:
char Screen::*pmf2(Screen::pos, Screen::pos) 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()); // 与上句代码等价,含义是调用pmf函数,并用该函数的返回值作为指针指向成员运算符.*的运算对象,但pmf不是函数,因此会报错
函数调用运算符的优先级比较高,在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:retType (C::*p)(parms)
(声明时,作为成员指针类型)和(obj.*p)(args)
(调用时)。
成员指针类型别名:
using Action = char (Screen::*)(Screen::pos, Screen::pos) const; // Action类型成员指针可指向返回类型为char,参数为两个Screen::pos的Screen的成员函数
Action get = &Screen::get; // get指向Screen的get成员函数
我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型:
Screen &action(Screen &, Action = &Screen::get);
成员指针函数表:
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[]; // 函数表
};
Screen::Action Screen::Menu[] = { &Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down
};
数组Menu依次保存每个光标移动函数的指针,这些函数按照Directions中枚举成员对应的偏移量存储,move函数接受一个枚举成员并调用相应函数:
Screen &Screen::move(Directions cm) {
return (this->*Menu[cm])();
}
myScreen.move(Screen::HOME);
要想通过一个成员函数指针进行函数调用,必须在调用时使用.*或->*运算符将该指针绑定到特定的对象上,因此它与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持调用运算符。
因为成员指针不是可调用对象,因此不能直接将它传给算法:
auto fp = &string::empty; // 指向string的empty函数
find_if(svec.begin(), svec.end(), fp); // 错误
string中保存的元素类型为char:
string s = "abc";
auto c = s[0]; // c被推断为char
可以用function模板类从指向成员函数的指针获取可调用对象:
function<bool (const string &)> fcn = &string::empty; // 将function对象绑定在成员函数上时第一个形参总是将empty所属类的对象的引用或指针传递给empty以表明是在哪个对象上使用的
find_if(svec.begin(), svec.end(), fcn);
如上,当输入给find_if函数的迭代器指向的类型为string时,本质上相当于执行了:
((*it).*p)() // p是function对象内部的成员指针,it是find_if的内部迭代器,*it是给定迭代器范围内的一个string对象,function知道如何正确使用.*来使用成员指针
如果svec中存放的是指向string的指针:
vector<string *> pvec;
function<bool (const string *)> fp = &string::empty; // 以指针而非引用表示empty所属的string对象
find_if(pvec.begin(), pvec.end(), fp); // fp接受一个指向string的指针,然后使用((*it)->*p)(),function对象知道使用迭代器所指对象的->*调用empty
function使用时要提供成员的调用形式,C++ 11新引入了mem_fn函数,它也定义在头文件functional中:
find_if(svec.begin(), svec.end(), mem_fn(&string::empty)); // 效果与以上使用function对象的效果相同,vector svec中可以存放string也可存放string *
mem_fn函数生成的可调用对象可以通过对象调用,也可以通过指针调用:
// svec是存放string的vector
auto f = mem_fn(&string::empty);
f(*svec.begin()); // 传入string对象,通过.*调用empty
f(&svec[0]); // 传入string对象的指针,通过->调用empty
如上,mem_fn生成的可调用对象相当于重载了调用运算符,一个接受string对象,一个接受string对象的指针。
使用bind(头文件为functional)生成可调用对象:
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1); // bind用于成员函数时必须将第一个参数设为表示
bind与function一样,需要将表示this的隐式形参转换成显式的;与mem_fn一样,它们都可以传入对象的引用或指向对象的指针。
auto f = bind(&string::empty, _1);
f(*svec.begin()); // 正确
f(&svec[0]); // 正确
一个类可以定义在另一个类的内部,称其为嵌套类或嵌套类型,它常用于定义作为实现部分的类(如文本查询中的QueryResult类)。
嵌套类是一个独立的类,外层类对象和嵌套类对象是相互独立的,嵌套类的对象中不包含任何外层类定义的成员,反之亦然。
嵌套类的名字在外层类作用域中可见,在外层类作用域外不可见。
嵌套类也使用访问限定符控制外界对其成员的访问权限。外层类对嵌套类成员没有特殊的访问权限,反之亦然。
嵌套类是外层类的一个类型成员,它的访问权限由外层类决定。
class TextQuery {
public:
class QueryResult; // 稍后定义
};
QueryResult是一个类型成员,必须先声明后使用(先在类内声明,之后才能作为query成员函数的返回类型使用)。
嵌套类的定义可以在类的内部或类的外部:
class TextQuery::QueryResult { /*...*/ }; // 类外定义,在类外定义之前,它都是一个不完全类型
以上将QueryResult类定义为类型成员后类中代码的唯一改变是不用在QueryResult中定义line_no成员了,因为该成员属于TextQuery,QueryResult可以直接访问它。
类外定义嵌套类成员函数:
TextQuery::QueryResult::QueryResult(string s, shared_ptr<set<line_no>> p, shared_ptr<vector<string>> f) : sought(s), lines(p), file(f) { }
嵌套类的静态成员定义:
int TextQuery::QueryResult::static_mem = 1024;
名字查找时先在嵌套类中,再到外层类中,再到外层类的外层作用域中查找。
类外定义返回类型为嵌套类的外层类方法:
TextQuery::QueryResult TextQuery::query(const string &sought) const { // 返回类型必须指明是嵌套类,因为是类外定义,外层类外作用域使用该类必须加作用域运算符
// 即使嵌套类是无法访问的也正确,因为这仅仅是说明返回类型,如果类外定义嵌套类则只能定义可访问的内层类的对象
// 处理
return QueryResult(sought, nodata, file); // 外层类作用域内直接可以访问到QueryResult,函数名query后即进入外层类作用域
}
union联合是一种特殊的类,它可以有多个数据成员,但在任意时刻只有一个数据成员可以有值,给union的某个数据成员赋值后,该union的其他成员就变成未定义的状态了。分配给union的存储空间至少要容纳它的最大的数据成员。union定义了一种新类型。
union不能含有引用类型成员,C++ 11中,含有构造函数或析构函数的类类型也可以作为union的成员类型。union可以为其成员指定public、protected、private等保护标记。默认,union的成员是public的。
union可以定义构造函数、析构函数等成员函数:
union Token {
char cval;
int ical;
double dval;
Token() : cval('g') { }
};
Token t; // 默认初始化
cout << t.cval << endl; // 输出g
union不能继承自其它类,也不能作为基类,因此union中不能有虚函数。
union可以让我们表示一组类型不同的互斥值。
定义union:
union Token {
char cval;
int ical;
double dval;
};
union的名字可选。
默认,union是未初始化的,我们可以像显式初始化聚合类一样使用一对花括号内的初始值显式初始化一个union:
Token first_token = { 'a' }; // 初始化cval
Token last_token; // 未初始化Token对象
Token *pt = new Token; // 指向未初始化的Token对象的指针
如果提供了初始值,初始值将会初始化第一个成员。
访问union对象成员:
last_token.cval = 'z';
pt->ival = 42;
匿名union是未命名union:
union {
char cval;
int ival;
double dval;
}; // 此时编译器自动创建一个未命名的union对象
cval = 'c'; // 为未命名的union对象赋值
ival = '42'; // cval失效
在匿名union的定义所在的作用域内,该union的成员是可以直接访问的。
匿名union不能包含非public成员,也不能定义成员函数。
C++ 11之前,union中不能含有定义了构造函数或拷贝控制成员的类类型。
我们将union的值改为类类型成员对应的值时,运行该类类型成员的构造函数;将类类型的值改为其他类型的值时,运行该类类型成员的析构函数。
当union包含的全是内置类型成员时,编译器会合成默认构造函数或拷贝控制成员。但如果union中含自定义了默认构造函数或拷贝控制成员的类类型,则编译器将为union合成对应的版本并将其声明为删除的。但我自己测试时以下代码也会报错,原因未知:
class c {
public:
int a = 3;
};
int main() {
union u {
c cObj; // c中未自定义默认构造函数或拷贝控制成员,如果去掉c中的a成员则通过编译
};
u uObj; // 报错u已将析构函数隐式定义为“已删除”
}
如某个类中含一个union成员,且该union含删除的拷贝控制成员,则该类的拷贝控制操作也是删除的。
由于union中管理构造或析构类类型成员比较复杂(每次切换都要显式执行当前类类型值的析构函数和新类类型值的构造函数),我们通常把含有类类型成员的union内嵌在另一个类(以下例子中的Token类)中,这个类可以管理并控制与union的类类型成员有关的状态转换。为了追踪union中储存了什么类型的值,通常定义一个独立的对象,该对象称为union的判别式。为了保持union与其判别式同步,我们也将判别式作为Token的成员,Token将定义一个枚举类型成员作为union判别式来追踪其union成员的状态:
class Token {
public:
Token() : tok(INT), ival(0) { }
Token(const Token &t) : tok(t.tok) { // 初始化列表中可以使用同类型参数(包括引用和指针)的私有成员
copyUnion(t);
}
Token &operator=(const Token &);
~Token() {
if (tok == STR) { // 如果union中含一个string成员,则销毁它
sval.~string();
}
}
// 各种赋值运算符
Token &operator=(const std::string &);
Token &operator=(char);
Token &operator=(int);
Token &operator=(double);
private:
enum { INT, CHAR, DBL, STR } tok; // tok的类型就是当前这个未命名的enum类型
union {
char cval;
int ival;
double dval;
std::string sval;
}; // 此处的union放到类外定义就会出错,不知道为什么
void copyUnion(const Token &); // 依赖于参数Token的union中实际值的类型的赋值过程
};
Token &Token::operator=(int i) {
if (tok == STR) {
sval.~string();
}
ival = i;
tok = INT;
return *this;
}
Token &Token::operator=(const std::string &s) {
if (tok == STR) {
sval = s;
} else {
new (&sval) string(s); // 定位new,在sval的内存处构造string
}
tok = STR;
return *this;
}
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 = t.dval;
break;
case Token::STR :
new (&sval) string(t.sval);
break;
}
}
Token &Token::operator=(const Token &t) {
if (tok == STR && t.tok!= STR) { // 如左侧运算对象的union值是string,但右边的不是,需要释放左侧的string
sval.~string();
}
if (tok == STR && t.tok == STR) { // 如果左右两侧运算对象的union都是string,直接用string的=运算符即可
sval = t.sval;
} else { // 以上两种情况都是左侧运算对象是string类型时的情况,其他情况直接赋值即可
copyUnion(t); // 赋值
}
tok = t.tok;
return *this;
}
上例中的union成员有定义了析构函数的类型(string),而union销毁时其类成员无法自动销毁,因为析构函数不清楚union存储的值是什么类型,因此需要为union定义一个析构函数以调用该类类型成员的析构函数。
上例代码中Token被默认初始化时(即调用默认构造函数时),编译器会将第一个成员初始化为0。
一个union中,只能给一个成员赋初值,当默认初始化该union时,被赋初始值的成员被初始化:
union u {
char cval;
int ival = 42;
double dval;
long lval = 11111; // 错误,只能有一个成员有初始值
};
u uval;
cout << uval.ival << endl; // 输出42
类可以定义在函数内部,称其为局部类。局部类只在定义它的作用域内可见。局部类所有成员都必须完整定义在类的内部。
局部类中不能声明静态数据成员。
局部类不能使用函数作用域中的普通局部变量,只能访问外层作用域中定义的类型名、静态变量、枚举成员以及全局变量:
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是foo的局部变量
barVal = ::val; // 正确,使用一个全局对象
barVal = si; // 正确,使用static局部对象
locVal = b; // 正确,使用一个枚举成员
}
};
}
外层函数对局部类的非公有成员没有任何访问特权,但局部类可以将外层函数声明为友元。
局部类中的名字查找也是由内到外。
局部类内部可以再嵌套类,此时,嵌套类的定义可以出现在局部类外,但要定义在与局部类相同的作用域中。
局部类内的嵌套类也是一个局部类,因此,这个嵌套类的所有成员必须定义在其内部。
不可移植的特性指因机器而异的特性,如算术类型的大小、位域、volatile限定符、链接指示(C++中新增的)。
类可以将其非static成员定义成位域,一个位域中有一定数量的二进制位,当程序需要向其他程序或硬件设备传递二进制数据时会用到。位域在内存中的分布是机器相关的。
位域的类型必须是整型或枚举类型,通常我们用无符号类型保存一个位域,因为带符号位域的行为是由具体实现确定的。
声明位域:在成员名之后紧跟一个冒号以及常量表达式,用于指定成员所占的二进制位数:
typedef unsigned int Bit;
class File {
Bit mode : 2; // mode占2位
Bit modified : 1;
public:
enum modes { READ = 01, WRITE = 02, EXECUTE = 03 }; // 8进制表示
File &open(modes);
};
类内部连续定义的位域可能会被压缩在同一整数的相邻位。
取地址运算符不能作用于位域,任何指针都无法指向类的位域。
使用位域:
void File::write() {
modified = 1;
// ...
}
可以使用内置的位运算符操作超过1位的位域:
File &File::open(File::mode m) {
mode |= READ; // 默认设置READ
/* ... */
if (m & WRITE) { // 如果m模式设置了WRITE
/* ... */
}
}
如类定义了位域成员,通常也会定义检查或设置位域值的内联成员函数:
inline bool File::isRead() const {
return mode & READ;
}
volatile限定符指明的数据元素可能在程序的控制或检测之外被改变,如一个被系统时钟定时更新的变量,这个关键字告诉编译器不用对其进行优化。
volatile使用方法:
volatile int display_register;
volatile Task *curr_task; // curr_task指向一个volatile对象
volatile int iax[max_size]; // 数组中每个都是volatile对象
volatile Screen bitmapBuf; // bitmapBuf对象的每个成员都是volatile的
volatile与const不冲突,可以同时使用。
一个类可以将方法定义成volatile的,这样才能被volatile对象调用。
volatile限定符用于指针时,与const相似:
volatile int v;
int *volatile vip; // volatile指针,指向int
volatile int *ivp; // 指向volatile int
volatile int *volatile vivp; // 指向volatile int的volatile指针
int *ip = &v; // 错误,必须使用指向volatile int的指针
ivp = &v; // 正确
vivp = &v; // 正确
与指针类似,volatile对象不能绑定到非volatile引用上。
我们不能使用合成的移动、拷贝构造函数或赋值运算符初始化volatile对象或从volatile对象赋值,因为合成的成员接受的形参是非volatile的常量引用。
如果希望拷贝、移动或赋值一个类的volatile对象,则该类必须自定义操作,这些自定义操作的形参类型指定为const volatile的引用即可。
C++有时需要调用其他语言编写的函数,常见的是调用C语言编写的函数,其他语言中的函数名也必须在C++中声明,声明时要指定返回类型和形参列表。C++使用链接指示指出任意非C++函数用的语言。
要想把C++代码和其他语言代码放在一起使用,需要我们有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。
链接指示有两种:单个的和复合的。链接指示不能出现在类定义或函数定义内部:
extern "C" size_t strlen(const char *); // 单语句链接指示
extern "C" { // 复合语句链接指示
int strcmp(const char *s1, const char *s2); // s1>s2时返回值>0,s1<s2时返回值<0,s1=s2时返回0
char *strcat(char *s1, const char *s2); // 将s2添加到s1后面,但要保证s1不会溢出,s2会覆盖s1末尾的'\0',并且在最后添加一个'\0',返回指向s1的指针,s1和s2所指内存区域不能重叠
}
extern后面的字符串字面值常量指出了编写函数所用的语言。上例的编译器应支持对C语言的链接指示。
多重声明的形式可以应用于整个头文件:
extern "C" {
#include <string.h> // 头文件中所有普通函数声明都被认为是由链接指示的语言(C)编写的
}
链接指示可以嵌套,如上例的string.h中还可以有其它链接指示的函数。
C++从C语言继承的标准库可以定义成C函数,但并非必须。
编写函数所用的语言是函数类型的一部分,因此,使用链接指示的函数的每个声明必须使用相同的链接指示,指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示:
extern "C" void (*pf)(int); // pf指向一个C函数
指向C函数的指针不能指向C++函数,反之亦然。但有的编译器允许这么做。
链接指示对返回类型和形参类型中的函数指针类型也有效:
extern "C" void f1(void (*)(int)); // f1的形参是指向C函数的指针
链接指示同时作用于声明语句中所有函数,因此当我们想传给C++函数一个指向C函数的指针时,必须使用类型别名:
extern "C" typedef void FC(int);
void f2(FC *); // f2是C++函数,形参是C函数指针
导出C++函数到其他语言:
extern "C" double calc(double dparm) { /* ... */ } // calc的定义,可被c程序调用
多种语言共享的函数的返回类型或形参类型受到很多限制,如不太可能把C++类的对象传给C程序,因为C程序无法理解构造函数、析构函数等。
为了允许在C或C++中都能编译同一个源文件,可以使用预处理变量__cplusplus:
#ifdef __cplusplus // 当我们正在编译C++程序时
extren "C"
#endif
int strcmp(const char *, const char *);
当C++函数转换的目标语言支持函数重载时,则为该语言实现链接指示的编译器很可能也支持重载这些同名C++函数。
C不支持函数重载:
extern "C" void print(const char *);
extern "C" void print(int); // 错误,C中没有同名函数
如果一组重载函数中有一个是C函数,则其余必定是C++函数。