C++primeplusp(p356-386)

1. 类的自动转换和强制类型转换

(1)内置类型的转换

实际上,在使用C++标准内置类型的时候,会发现,将一种类型变量赋值给另一种类型变量的时候,往往会自动进行类型转换。

-----在计算表达式的时候,C++将bool,char,unsigned char,signed char和short值转换为int。这叫整型提升

why?
因为在计算机进行计算的时候,使用int类型运算速度快,所以在计算的时候,先转换为int,再转换为原本类型可能看起来麻烦,但是计算速度仍然比使用别的类型直接计算速度要快。打个比方:《《用筷子夹菜固然很好,但是太慢,直接取一个大汤勺来,那么一次可以搞到很多菜,很方便很快。》》

这个可能举例不太恰当
,看这个:
在这里插入图片描述

下面展示一个 例子

// A code block
short a=20;
short b=40;
short sum=a+b;

那么实际上,C++先将a,b转换为int,然后相加,再将结果转换为short。

C++校验表规定如下:

(1)如果有一个操作数的类型是long long ,则将另一个类型转换为long double
(2)如果有有一个操作数是double,则将另一个转换为double
(3)如果有一个操作数是float,则将另一个转换成float
(4)否则,说明操作数都是整型,因此执行整型提示(比如charshortunsigned char等等)
(5)在这种情况下,如果两个数是同符号,且其中一个操作数的级别比另一个低,则转换为级别高的类型
(6)否则,如果有符号类型可以表示无符号类型的所有可能取值,则将无符号操作数转换成有符号操作数的类型
(7)否则,将两个操作数都转换成有符号类型的无符号版本

提示:
(1)有符号整型的级别从低到高:
long long , long , int , short , signed char.
无符号整型的级别和有符号整型的级别相同

(2)类型char ,unsigned char , signed char 级别相同。 类型bool 级别最低。 wchar_t ,char16_t ,char32_t 的级别与其底层类型相同

(2)自定义类型的转换

如果要将一个类型转换为另一个类型,一般使用构造函数+赋值函数来实现。
举个例子:

有一个类对象Stonwt,想要将一个double类型变量转换为Stonwt类型变量,很显然C++不能默认做到这种事情,
所以需要接受double类型参数的构造函数
Stonwt(double a);   //转换函数,构造函数
Stonwt mycat;    //建立一个对象
mycat=19.8;   //将double类型转换为Stonwt类型

只接受一个参数的构造函数才能作为转换函数,两个参数不行。上面的例子就是一个转换函数。上面的程序说明: —程序使用构造函数Stonwt(double a)创建一个临时对象,然后将19.8作为初始化值,采用逐成员赋值的方式将该临时对象的内容复制到mycat中。这一过程叫隐式转换,这是因为它是自动调用的。

如果想要关闭隐式转换,需要加上关键字explicit

explicit Stonwt(double a);
Stonewt mycat;
mycat=19.6;  //这将不会调用转换构造函数,将是错误的。
//如果没有explicit,那么首先调用stonwt(double a),创建一个临时对象,然后将19.6的值
//初始化这个临时变量的私有成员,下一步,将这个临时对象的所有变量一一复制粘贴给mycat这个实体
//下一步,销毁临时对象。19.6(double类型)成功转换为Stonwt类型.
//但是有了explicit之后,就不能这样了,但是可以显示转换
mycat=Stonewt(19.6);  //这就是很自然的了,步骤原理和上面一样,不重复说
mycat=(Stonewt)19.6;    //老版本写法,也可以

虽然但是,还要强调一遍!!这种转换构造函数只支持一个参数的情况!!!

转换函数
上面提到了转换函数,不过只是转换函数里面的一个分支,应该叫转换构造函数。而下面介绍另一种转换函数。
首先知道区别:构造函数只支持从某类型到类类型的转换,而转换函数可以进行从类类型到其它类型的转换。

1.格式
operator typename();
2.注意事项:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数没有参数

例如,转换为double类型的函数原型:operator double();
首先,原型指出了要转换成的类型double,因此不需要指定返回类型。再者,转换函数是类方法,这说明,它需要类对象来调用,从而告知函数要转换的值。所以不需要参数。

下面写一个小程序来理解上面的知识点:

#include<iostream>
using namespace std;
class Stonewt
{
private:
	enum { Lbs_per_stn = 14 };
	int stone;
	double pds_left;
	double pounds;
public:
	Stonewt(double lbs);   //转换构造函数
	Stonewt(int stn, double lbs);
	Stonewt();
	~Stonewt();
	void show_lbs()const;
	void show_stn()const;
	operator int()const;
	operator double()const;
};

Stonewt::Stonewt(double lbs)
{
	stone = int(lbs) / Lbs_per_stn;     //lbs是磅,stone是英石,是两种计量单位,可以相互转换,这里表示磅转换为英石
	pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs);    //lbs是pounds的缩写
	pounds = lbs;
}


Stonewt::Stonewt(int stn, double lbs)
{
	stone = stn;
	pds_left = lbs;
	pounds = stn * Lbs_per_stn + lbs;
}

Stonewt::Stonewt()
{
	stone = pounds = pds_left = 0;
}

Stonewt::~Stonewt()
{

}

void Stonewt::show_stn()const
{
	cout << stone << " stone, " << pds_left << " pounds\n";

}


void Stonewt::show_lbs()const
{
	cout << pounds << " pounds\n";
}

Stonewt::operator int()const
{
	return int(pounds + 0.5);
}

Stonewt::operator double()const
{
	return pounds;
}


int main()
{
	using namespace std;
	Stonewt pop(9, 2.8);
	double p_wt = pop;
	cout << "转换为double:";
	cout << "pop: " << p_wt << "pounds\n";
	cout << "转换为int:";
	cout << "pop: " << int(pop) << "pounds \n";
	return 0;
}

上面的程序有几点:

  • 使用cout输出pop的时候必须强制转换类型,如int(pop).否则编译器不知道使用哪个转化函数,会产生二义性,如果不想使用显示转换,一种方法就是指定义一个类型转换函数,比如去掉operator double.这样就不会产生二义性
  • 如果想要避免隐式抓换,应该使用关键字explicit。和前面说的explicit用法一样.

2.类的动态内存管理

(1)string_Bad

如果类对象需要存储姓名,那么常用的方法是声明一个固定大小的数组。那么这里不免存在一个问题,这个数组要开多大?如果开小了,会有一些名字存不进去,如果开大了,又会浪费内存。左右为难,因此,想到,使用动态内存是不是一种更加好的选择。。。下面看一个存在问题的小程序,至于有什么问题,可以上机实验一下,后面会详细解答。

#include<iostream>
using namespace std;

class StringBad     
{
private:
	char* str;    //成员是一个指针,这里要注意,后面任意出错
	int len;     //记录指针指向的字符串的长度
	static int num_strings;      //静态成员,所有对象共享同一个静态成员,单独在内存中存在,不依赖于对象。
public:
	StringBad(const char* s);   //构造函数
	//StringBad(const StringBad & st);  //复制构造函数
	//StringBad& operator=(const StringBad& st);    //赋值函数
	const char& operator[](int i)const;
	StringBad();
	~StringBad();
	friend std::ostream& operator<<(std::ostream& os, const StringBad& st);   //友元函数,返回类型必须是ostream&
	//因为ostream类没有公共函数创建临时ostream对象
};

int StringBad::num_strings = 0;

StringBad::StringBad(const char* s)
{
	int len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
	cout << num_strings << ":\"" << str << "\"object created\n";
}

/*StringBad::StringBad(const StringBad& st)   //复制构造函数
{
	num_strings++;
	int len = std::strlen(st.str);
	str = new char[len + 1];
	std::strcpy(str, st.str);
	cout << num_strings << ";\"" << str << "\"object created\n";
}*/

StringBad::StringBad()
{
	int len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << ";\"" << str << "\"defualt object created\n";
}


StringBad::~StringBad()   //析构函数
{
	cout << "\"" << str << "\"object deleted, ";
	--num_strings;
	cout << num_strings << " left\n";
	delete[]str;
}

std::ostream& operator<<(std::ostream& os, const StringBad & st)
{
	os << st.str;
	return os;
}

/*StringBad& StringBad::operator=(const StringBad& st)
{
	if (this == &st)
		return *this;
	delete[]str;
	int len = strlen(st.str);
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}*/

const char& StringBad::operator[](int i)const
{
	return str[i];
}

void callme1(StringBad& rsb)   //引用传参,
{
	cout << "通过引用传递\n";
	cout << "\"" << rsb << "\"\n";   //rsb的类型是StringBad&
}

void callme2(StringBad sb)   //这是错在哪里了?//因为默认的复制构造函数只简单复制指针的地址,而临时对象sb结束后无意中将原来对象的字符串析构了,所以要自己定义一个复制构函数
{  //这看起来好像没有什么问题,其实问题大得很
	cout << "通过值传递:\n";
	cout << "\"" << sb << "\"\n";   //sb的类型是StringBad
}


int main()
{
	
	{
		cout << "开始于内部块.\n";    
		StringBad headline1("天使小短裙!");
		StringBad headline2("我是恶魔之王!");
		StringBad sports("我喜欢打篮球!");
		cout << "headline1: " << headline1 << endl;
		cout << "headline2:" << headline2 << endl;
		cout << "sports:" << sports << endl;
		callme1(headline1);
		cout << "headline1: " << headline1 << endl;
		callme2(headline2);
		cout << "headline2:" << headline2 << endl;
		cout << "初始化新对象(从已有的对象):\n";
		StringBad sailor = sports;   //使用默认赋值运算符,如果没有显示重载赋值运算符,那么系统会自动为类重载赋值运算符
		cout << "sailor: " << sailor << endl;
		cout << "分配一个对象给另一个:\n";
		StringBad knot;
		knot = headline1;
		cout << "knot; " << knot << endl;
		cout << "退出内部块:\n";


	}
	cout << "End of main()\n";
	return 0;

}

问题发生于一些特殊成员函数,这些成员函数是自动定义的,然而,这些自动定义的成员函数有时候很好用,但是有时候却又不行:

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 复制构造函数(浅复制),如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义

C++提供上面一些自动函数,就是没有人为操作,也仍然存在的函数。
而这个小程序·的·问题是由于:隐式复制构造函数和隐式赋值运算符引起的。

在解决问题之前,介绍这几个函数;

  1. 默认构造函数
    简单的说,如果定义类的时候没有构造函数,C++将创建一个构造函数,这个构造函数有3个特点
    (1)自动生成 (2)没有参数 或者参数都是已知量(默认值) (3)初始值未知
    而且注意,默认构造函数只能有一个,否则会出现歧义。

  2. 复制构造函数
    复制构造函数用于将一个对象复制到新的对象中。它用于初始化阶段。原型Class_name(const Class_name&); 它接受一个指向类的对象的常量引用作为参数。例如,StringBad类的复制构造函数的原型如下:
    StringBad(const StringBad&);
    对于复制函数要注意,何时调用和有什么功能

  3. 什么时候调用复制函数

(1)新建一个对象并将其初始化为同类现有对象时,复制构造函数将被调用;
StringBad ditto(motto); //调用复制构造函数
(2)每当生成对象副本,都会调用复制构造函数
StringBad also=StringBad(motto); //先调用复制构造函数生成一个motto对象的副本,然后编译器查看是否有显示定义赋值运算符重载相匹配,如果没有,将调用默认赋值构造函数,将motto对象的副本赋给also;但不管怎么样,首先是调用复制构造函数生成motto的副本,但是这种复制是“浅复制”(其原因就是在于这种复制构造函数是默认的,不是我们自己定义的)

  1. 默认复制构造函数

上面说的复制构造函数都是默认的,那么默认的复制构造函数是个什么原理呢?
默认构造函数逐个复制非静态成员(成员复制也叫“浅复制”,),复制的是成员的值
如下:
StringBad sailor=sports;

等价于下面(只是由于私有成员不能访问,所以下面这些代码是无法通过编译的);

StringBad sailor;
sailor.str=sports.str;
salior.len=sports.len;

问题1 那么到这里,就知道了上面的程序的问题之一出现在了哪里,就是默认复制构造函数的“复制”上出了问题。
很明显,StringBad的成员里面有一个指针str,那么当调用复制构造函数的时候,指针也会复制,但是复制的是指针的值,也就是说把地址复制了一遍,那么会这样;
sailor.str=sports.str; //仅仅复制字符串地址

那么两个对象相当于还是共享一个字符串,那么当临时对象析构的时候,delete[]str,新对象的str也同时被析构,那么这就导致数据损失。当下次析构新对象的时候,会发现对象成员str之前已经被释放了。当系统发现一个地方被释放两次,会报错(GPF)

解决问题1
那么默认复制构造函数不行,就要自定义一个显示复制构造函数

String::StringBad(const StringBad &st) //显示复制函数,如果类型匹配,系统优先调用这个函数而不是默认的复制构造函数
{
num_strings++;
len=strlen(st.str);
str=new char[len+1];
std::strcpy(str,st.str);
cout<<num_strings<<“:”“<<str<<”“object created\n”;
}

问题2
当然,上面程序的问题不只一个,还有一个,那就是赋值运算符。
原型如下:
Class_name & Class_name::operator=(const Class_name &) ;
它接受并返回一个指向类对象的引用,例如,StringBad类的赋值运算符的原型如下:
StringBad & StringBad::operator=(const StringBad &) ;

(1)赋值运算符的功能以及何时使用
将已有的对象赋给另一个对象时,将使用重载的赋值运算符:
StringBad headline1(“i am a boy!”) ;

StringBad knot ; //使用构造函数创建一个对象
knot=headline1 ; //使用赋值运算符将headline1对象的成员的值一一赋值给knot。

初始化对象时,并不一定会使用赋值运算符:
比如:StringBad metoo=knot ;
那么,这句话的意思是可能有两种情况:
1.使用复制构造函数创建一个临时对象,然后通过赋值将临时对象值复制到新对象中。
2.直接使用赋值运算符赋值。(可能)。

但是上面的knot=headline1;这句话的意思就是:使用默认赋值运算符将成员的值一一赋值。这就导致和默认复制构造函数一样的问题,指针的赋值—没有新开辟一个堆空间来存放一个新的字符串副本,而是仅仅复制字符串存在的空间位置,也就是说,新的对象里面的str和老的对象的str是一个东东,这就会在对象析构的时候出现问题,因为一个字符串不能被析构两次。也就是说,析构knot的时候没有问题,但是析构headline1的时候就出现了问题。

解决问题2
那么如何解决这个赋值的问题?
提供赋值运算符(进行深度赋值)

StringBad  &StringBad::operator=(const StringBad &st)
{
	if(this==&st)
		return *this;
	delete[]str;
	len=strlen(st.str);
	str=new char[len+1];
	std::strcpy(str,st.str);
	return *this;
}


如此一来,这个程序就没有什么问题了。

3.使用中括号表示法访问字符

平常使用中括号来访问字符串其中某一个字符。
char s[10];
cout<<s[0]<<" "<<s[1]<<endl;

C++允许重载这个符号’[]';
这个运算符有两个操作数,比如arr[i],那么arr时第一个操作数,i是第二个操作数;

假设opera是一个String对象
String opera(“i am lby”);
char& String ::operator[](int i)
{
return str[i];
}
使用opera[4],相当于调用:opera.operator [] (4)
cout<<opera[4]被转换为cout<<opera.operator[] (4);

还可以这样:
opera[0]=‘w’;
这句话这样运作:opera.operator[] (0)=‘w’ , 由于左边返回一个char&的值,所以相当于
opera.str[0]=‘w’;

4.静态类成员函数

可以将成员函数声明为静态的。
但是有几点:

  1. 函数声明要包含关键字static
  2. 如果函数定义是独立的,则不能有关键字static.
  3. 不能通过对象调用静态成员函数
  4. 如果静态成员函数在公有部分声明过,则可以使用类名和作用域解析运算符来调用它。

例如:

static int ALLength()
{
	return xx;    //注意,xx也只能是静态数据成员,因为静态成员函数不与特点对象相关联,不能访问类的私有数据。
}

//使用
int count=String::ALLLength();

5.返回对象注意

1.如果方法或函数要返回局部对象,则应该返回对象,而不是指向对象的引用,原因就是局部变量的生命周期随着函数调用完成也结束了。所以,这个时候使用复制构造函数来生成返回的对象是不可避免的(先将局部变量拷贝一个副本当作临时变量,然后临时变量返回给调用对象,最后临时变量死亡)
2.如果方法或函数要返回一个没有公有(public)复制构造函数的类(如ostream类)的对象,那么它必须返回一个指向该对象的引用,如果是这种情况,则使用引用,这样效率更高。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值