C++ Primer笔记(十九)

控制内存分配

重载new和delete

使用一条new表达式时:

string *sp = new string("a value");
string *arr = new string[10]; 

首先new表达式调用名为operator new的标准库函数,分配足够大,原始的,未命名的内存空间以存储特定类型的对象,然后编译器运行相应的构造函数以构造这些对象并传入初始值。

使用delete表达式delete sp delete [] arr 第一步对sp所指的对象或者arr所指数组中元素执行对应的析构函数,第二步调用名为operator delete的标准库函数释放空间。

  1. 调用new/delete 表达式,首先查找函数,如果是类类型,则先在该类及其基类的作用域中查找,如果此类含有operator new/delete,则调用这些成员,否则在全局作用域中查找,如果找到了匹配的版本,则使用该版本执行new/delete

  2. 对于new 返回类型必须是void*第一个形参为size_t且不能有默认实参,编译器调用时将存储指定类型对象所需字节数传递给size_t实参。也可以为其提供额外形参来定义operator new,但是不能重载如下形式void *operator new(size_t, void*)

  3. 对于delete 返回类型必须是void,第一个形参类型为void*,如果定义为类的成员,可以包含另外一个类型为size_t的形参,值为第一个形参所指对象的字节数。(常用于删除继承体系中对象)

void *operator new(size_t size)
{
	if(void *mem = malloc(size))
		return mem;
	else
		throw bad_alloc();
}
void operator(void* ptr)noexcept{free(ptr);}

定位new表达式

operator new 和operator delete 负责分配或者释放内存空间,但不会构造或者销毁对象
而需要在分配的内存空间上构造对象,用定位new表达式

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {braced initializer list} 

仅通过地址调用时,定位new使用operator new(size_t, void*) 该函数不负责分配内存,只是简单地返回指针实参,并由new 表达式在该地址上初始化对象。

  1. 在使用allocator类时,传给construct的指针必须指向同一个allocator类分配的空间,但传给定位new表达式的指针无需指向operator new 分配的内存。
  2. 对析构函数的显式调用也与使用destroy很类似,string *sp = new string("a value"); sp->~string(); 调用析构函数会销毁对象,但不会释放内存。

运行时类型识别

由typeid 和 dynamic_cast 运算符实现,当这两运算符用于某种类型的指针或引用时,且该类型含有虚函数,运算符使用所绑定对象的动态类型。RTTI实际上替代了虚函数,实现了非虚函数操作的动态绑定

dynamic_cast运算符

dynamic_cast<type*>(e)//e是指针
dynamic_cast<type&>(e)//e是左值
dynamic_cast<type&&>(e)//e是右值

type是类类型,且该类型有虚函数,e必须:是type的公有派生类或是type的公有基类或者类型相符。
如果转换目标是指针,失败的话语句结果为0,如果转换目标是引用,失败抛出bad_Cast异常

if(Derived *dp = dynamic_cast<Derived*>(bp))
{
		
}else//bp指向base
{
}

bp是base类型指针,但是如果bp指针指向的是Derived类型对象,那么语句初始化dp,并令其指向bp所指的Derived对象。否则类型转换的结果为0,去执行else语句的操作。

void f(const Base &b)
{
	try{
		const Derived &d = dynamic_cast<const Derived&>(b);
	}catch(bad_cast){
	}	
}

上为引用类型的dynamic_cast

typeid运算符

typeid(e) 可以作用于任意类型表达式,顶层const被忽略,如果是引用就返回该引用所引对象的类型,作用于数组或函数时不会执行向指针的标准类型转换。我们使用typeid比较两条表达式的类型是否相同,

Derived *dp = new Dervied;
Base *bp = dp;
if(typeid(*bp) == typeid(*dp))//比较动态类型是否相同
{}
if(typeid(*bp)==typeid(Derived))//判断bp当前所指是否Derived对象
{}

typeid作用于指针时typeid§,返回的结果是静态编译时类型。只有当类型有虚函数时,编译器才会对表达式求值,如果类型不含有虚函数,typeid返回表达式的静态类型。typeid(p) 如果p所指的类型不含有虚函数,则p不必是一个有效的指针,否则p在运行时求值,若p为空,抛出异常

使用RTTI

为具有继承关系的类实现相等运算符,派生类的相等运算符必须考虑派生类的新成员,一般考虑定义虚函数,但是虚函数的不同版本必须具有相同形参,所以必须是基类的引用,此时只能使用基类的成员而不能使用派生类的成员

  1. 所以我们先用typeid检查两个对象是否一致,若一致则调用equal函数,在该函数中进行dynamic_cast转换为对应的类类型,否则返回false
bool operator==(const Base &lhs, const Base &rhs)
{
	return typeid(lhs)==typeid(rhs)&&lhs.equal(rhs);
}
  1. 调用虚equal函数
bool Derived::equal(const Base &rhs) const
{
	auto r = dynamic_cast<const Derived&>(rhs);
	//执行比较操作并返回结果
}

将实参类型转换为派生类型,再进行比较

type_info类

type_info类没有默认构造函数,拷贝控制成员都被定义成删除的,所以创建type_info对象的唯一方法是使用typeid运算符;name方法返回类型的名字,但是名字因编译器而异,标准对该方法的唯一要求是不同类型返回的名字有区别

t1==t2同一种类型返回true
t1!=t2不同类型返回true
t.name()返回C风格字符串
t1.before(t2)返回bool值表示t1是否位于t2之前

枚举类型

将一组整形常量组织在一起,C++11引入了限定作用域的枚举类型enum class open_modes {input, output, append};
定义不限定作用域枚举类型省略关键字class enum color {red, yellow,green}; 如果enum是未命名的,则只能在定义该enum时定义它的对象,enum {a,b,c} object

  1. 限定作用域枚举类型,成员在作用域外是不可访问的。不限定作用域枚举类型,枚举成员作用域与枚举本身作用域相同。peppers p = peppers::red若没有显式提供枚举初始值,则从0开始逐个+1,枚举成员是const。
  2. 要初始化enum对象或者为enum对象赋值,则必须使用该类型的一个枚举成员或者该类型的另一个对象
enum open_modes {input,output};
openmodes om = open_modes::input;

不限定作用域的枚举类型的对象或成员自动转为整形。

  1. enum是由某种整数类型表示的,我们可以自定义使用的整数类型
enum intValues : unsigned long long {
	charTyp = 255,intTyp = 65535, long_longTyp = 18446744073709551615ULL;
}

如果不指定,则默认情况下限定作用域的enum成员为int类型,而不限定作用域枚举类型,其成员不存在默认类型。如果指定了类型,但是值超出范围会引发程序错误。

  1. 枚举类型可以前置声明 enum intValues : unsigned long long; 不限定作用域的,指定成员类型;enum class open_modes 限定作用域的,使用默认类型int
  2. 要想初始化enum对象,必须用该enum类型的另一个对象或者枚举成员来初始化它,即使某个整数与成员的值相等,也不能作为实参使用
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形参
	return 0;
}

可以将不限定作用域的枚举类型或成员传递给整形实参,此时enum的值提升为int或更大的整形

void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL);//被提升为int 故匹配int形参
newf(uc);//匹配unsigned char 形参

类成员指针

成员指针是指向类的非静态成员的指针,其包含了类的类型及其成员的类型

数据成员指针

const string Screen::*pdata; 将pdata声明成一个指向Screen类的const string 成员指针。而初始化时再指定其所指的成员pdata = &Screen::contents

  1. 初始化成员指针时,该指针没有指向任何数据,成员指针指定了成员而非对象,解引用指针时才提供对象的信息。
Screen myScreen,*pScreen = &myScreen;
auto s = myScreen.*pdata;
s=pScreen->*pdata;
  1. 常规的访问控制对成员指针同样有效,所以我们一般不能获得数据成员的指针,通常定义一个函数返回指向某个成员的指针
class Screen {
public:
	static const std::string Screen::*data(){return &screen::contents;}
}

调用data函数时,返回指向Screen类的const string成员的指针。

指向成员函数的指针

auto pmf = &Screen""get_cursor; 指向成员函数的指针也需要指定目标函数的返回类型和形参列表,且包含const或者引用限定符。

char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;

Screen::*两端的括号必不可少,否则认为这是一个无效的函数声明。

  1. 使用成员函数指针
Screen myScreen, *pScreen = &myScreen;
char c1 = (pScreen->*pmf)();//因为函数调用运算符优先级高,所以需要两边加括号。

使用typedef或者别名可以更容易理解成员指针

using Action = char (Screen::*)(Screen::Pos, Screen::Pos) const;
Action get  = &Screen::get;
Screen& action(Screen&, Action = Screen&::get);

该类型表示指向Screen类的常量成员函数的指针,接受两个形参,返回一个char。而action是包含两个形参的函数,一个形参是Screen的引用,另一个是指向成员函数的指针,接受两个pos形参并返回一个char。调用方法action(myScreen); action(myScreen, &Screen::get)

  1. 函数指针常见用法是存入函数表中,
class Screen{
public:
	using Action = Screen& (Screen::*)();
	enum Directions {HOME,FORWARD,BACK,UP,DOWN};
	Screen& move(Directions);
	Screen& hone();
	Screen& forward();
	Screen& back();
	Screen& up();
	Screen& down();
private:
	static Action Menu[];//函数表
};
Screen& Screen::move(Directions  cm)
{
	return (this->*Menu[cm])();//运行某个成员函数
}
Screen::Action Screen::Menu[]={	&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&SCreen::down};

将成员函数用作可调用对象

成员指针不是一个可调用对象,必须先利用.* 或者->*将其绑定到对象上,所以不能将其作为实参。
解决方法是使用标准库模板function来显式实例化模板,需要指定函数的返回值和形参,并使function对象包含指向成员函数的指针,定义function对象时,必须指定对象所能表示的函数类型,本例子中为返回值bool 接受string参数,如果是成员函数,则第一个形参表示该成员在哪个对象上执行。

vector<string*> pvec;
function<bool (const string&)> fcn = &string::empty;
find_if(pvec.begin(), pvec.end(),fp);
  1. 用mem_fn生成可调用对象
    men_fn可以根据成员指针的类型推断可调用对象的类型find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
    mem_fn生成的对象接受一个string实参,返回一个bool值,可以通过指针或者对象调用。
auto f = mem_fn(&string::empty);
f(*svec.begin());
f(&svec[0]);
  1. 用bind生成可调用对象
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));

将string绑定到empty的第一个隐式实参上。

嵌套类

  1. 定义在另一个类的内部的类称为嵌套类,嵌套类对象中不包含任何外层类定义的成员,外层类对象中也不包含嵌套类定义的成员。嵌套类的名字在外层作用域中是可见的。
  2. 位于外层类public部分的嵌套类实际上定义了可以随处访问的类型;位于外层类protected的部分嵌套类定义类型只能被外层类及其友元和派生类访问;位于外层类private的嵌套类只能被外层类的成员和友元访问
class TextQuery
{
public:
	class QueryResult;
};

上面只是声明了嵌套类,但没有定义,当在外层类之外定义嵌套类时,必须以外层类的名字限定嵌套类的名字

class TextQuery::QueryResult
{
	friend std::ostream& print(std::ostream&, const QueryResult&);
public:
	QueryResult(std::string, std::shared_ptr<std::set<line_no>>,std::shared_ptr<std::vector<std::string>>);
};

QueryResult可以直接访问line_no成员

  1. 定义构造函数时也需要外层类的名字限定嵌套类的名字TextQuery::QueryResult::QueryResult
  2. 嵌套类声明的静态成员,定义将在外层类作用域之外
  3. 嵌套类中的名字查找,外层类可以直接使用嵌套类的名字,比如
TextQuery::QueryResult TextQuery::query(const string& sought) const
{
	//……
	return QueryResult(sought,nodata,file);
}

先通过作用域标识符指明了返回值,在函数体内部可以直接访问QueryResult

  1. 外层类对象和嵌套类对象没有任何关系,嵌套类的对象只包含嵌套类定义成员, 外层类对象中只有外层类定义的成员。

union

  • 一个union可以有多个数据成员,但是在某个时刻只有一个数据成员可以有值,当给union的某个成员赋值之后,该union的其他成员就是未定义的了。
  • union不能含有引用类型的成员,但是可以为其成员指定public protected private等标记,默认 union的成员都是公有的。
  • union也可以定义包括构造函数和析构函数在内的成员函数,但union不能继承自其他类,也不能作为基类使用,所以不能有虚函数。
  1. union很方便地表示类型不同的互斥值
union Token{
	char cval;
	int ival;
	double dval;
}
  1. 默认union是未初始化的,我们使用花括号内初始值显式初始化union,初始值用于初始化第一个成员。
  2. 匿名union是未命名的union
union {
	char cval;
	int ival;
	double dval;
};

匿名union不能包含受保护的成员或者私有成员,也不能定义成员函数,但是可以直接访问其成员

含有类类型的union

如果想将union的值改为类类型成员对应的值,则必须运行该类型的构造函数/析构函数。我们通常将含有类类型成员的union内嵌在另一个类中,这个类来管理并控制与union类类型成员有关的状态转换。为了追踪union中到底存储了什么类型的值,通常我们定义一个独立的对象,该对象称为union的判别式,为了保持判别式与union同步,我们将判别式也作为Token的成员

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) sval.~string();}
	Token& operator=(const std::stirng&);
private:
	enum {INT,CHAR,DBL,STR} tok;//判别式
	union{
		char cval;
		int ival;
		double dval;
		std::string sval;
	};
	void copyUnion(const Token&);

析构函数检查被销毁的对象中是否存储着string值,如果有,则类的析构函数显式地调用string的析构函数

  1. 类的赋值运算符负责设置tok并为union的相应成员赋值,需要注意的是,string版本需要管理与string有关的转换
Token &Token::operator=(int i)
{
	if(tok==STR) sval.~string();//如果存储的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表达式
	tok=STR;
	return *this;
}

如果当前存储的不是string,则利用定位new表达式构造一个string,再更新表达式

  1. 拷贝控制成员需要先检验判别式以明确拷贝所采用的方式,故定义一个copyUnion的成员,在拷贝构造函数中调用copyUnion时,成员将被默认初始化,编译器会初始化union的第一个成员(显然不是string) 而在赋值运算符中,如果存储了string,则构造一个string
void Token::copyUnion(const Token &t)
{
	switch(t.tok)
	{
		case Token::INT: iva = t.val; break;
		case Token::CHAR: cval = t.cval; break;
		case Token::DBL: dval = t.dval;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;
}

如果左边string 右边不是,则先释放string再赋新值,如果都是string 直接赋值,如果左边不是右边是,调用copyUnion进行赋值,构造一个string

局部类

定义在某个函数内部的类称为局部类,局部类定义的类型只在定义它的作用域内可见,局部类的所有成员必须完整定义在函数内部,所以其访问规则与嵌套类相差很远

  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)
		{
			barVal = val;//错误 val是foo的局部变量
			barVal = ::val;//可以使用全局对象
			barVal=si//正确 可以使用静态对象
			locVal = b;//可以使用枚举
		}
}

外层函数对局部类的私有成员没有任何访问特权,但是局部类可以将外层函数声明为友元

  1. 局部类内部的名字查找与其他类相似,声明类的成员,首先确保用到的名字位于作用域中。定义成员用到的名字可以出现在类的任意位置,如果不是局部类的成员则在外层函数作用域中查找。
  2. 可以在局部类的内部再嵌套一个类,此时嵌套类的定义可以出现在局部类之外,不过必须出现在与局部类相同的作用域中
void foo()
{
	class Bar {
	public:
		class Nested;
	};
	class Bar::Nested{
	};
}

局部类的嵌套类也是一个局部类,所有成员必须定义在嵌套类的内部

不可移植特性

不可移植特性指的是因机器而异的特性,将程序从一台机器转移到另一台机器上,通常需要重新编写程序

位域

类可以将其非静态数据成员定义成位域,一个位域中含有一定数量二进制位

位域的类型必须是整形或者枚举类型,通常使用无符号整数保存一个位域,声明方法是成员名之后紧跟冒号和常量表达式,指定所占位数

typedef unsigned int Bit;
class File{
	Bit mode:2;
	Bit modified:1;
	Bit prot_owner:3;
	Bit prot_group:3;
	Bit prot_world:3;
public:
	enum modes{READ,WRITE,EXECUTE};

访问位域方式与访问其他数据成员方式类似

void File::write()
{
	modified = 1;
}
void File::close()
{
	if(modified)
		//…
}
File &File::open(File::modes m)
{
	mode |=READ;
	if(m&WRITE)
	return *this;
}

取地址运算符不能作用于位域,因此任何指针都无法指向类的位域

volatile限定符

当对象的值可能在程序的控制或者检测之外被改变时,应将该对象声明为volatile 告知编译器不得优化

volatile int display_register;
volatile Task* currTask;//指向volatile对象
volatile int iax[max_size];//数组每个元素都是vola
volatile Screen bitmapbuf;//每个成员都是volatile

可以将成员函数定义为volatile,只有volatile成员函数才能被volatile对象调用

  1. 与const相同,volatile修饰指针时也有顶层 底层规则
  2. 合成的拷贝控制成员无效,因为合成的接受的是非volatile的常量引用形参,明显是不能为volatile对象调用的,必须自定义拷贝控制成员,Foo(const volatile Foo&)从一个const volatile的Foo对象进行拷贝

链接指示

链接指示允许我们在C++中调用其他语言编写的函数,它不能出现在类定义或者函数定义的内部

extern "C" size_t strlen(const char*);
extern "C" {
	int strcmp(const char*, const char *);
	char *strcat()(char*, const char*);
}//复合类型指示
  1. 当#include 被放置在复合链接指示的大括号中,头文件中所有普通函数声明都被认为是连接指示编写的
extern "C"{
	#include <stdio.h>
}
  1. extern "C" void (*pf)(int) 指向c函数的指针,且指向c函数和指向c++函数指针是不同类型,不能对c函数指针执行初始化后再将c++函数名赋值给它。
  2. 当我们使用链接指示时,它对作为返回类型或者形参的函数也有效extern "C" void f1(void(*)(int)) 指明调用C函数f1时必须传给它一个c函数的名字或者指向c函数的指针。如果希望给C++函数传入C指针,则必须使用类型别名
extern "C" typedef void FC(int);
void f2(FC*);
  1. 有时需要在C和C++中编译同一个源文件,在编译C++版本程序预处理器会自动定义 __cplusplus,我们可以利用这个有条件包含代码
#ifdef __cplusplus
extern "C"
#endif
int strcmp(const char*, const char*)
  1. 不支持函数重载的链接指示,只能用于说明一组重载函数中的某一个,并且如果一组重载函数中有一个C函数,则其余必定是C++函数
class SmallInt;
class BigNum;
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

C版本的calc可以在C或者C++中使用,使用类类型形参的函数只能在C++函数中调用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值