• 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;
    
    • 1
    • 2
    • 3
    • 4

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

    C++校验表规定如下:

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

    只接受一个参数的构造函数才能作为转换函数,两个参数不行。上面的例子就是一个转换函数。上面的程序说明: —程序使用构造函数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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

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

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

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

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

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

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    上面的程序有几点:

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

    2.类的动态内存管理

    (1)string_Bad

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

    #include
    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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

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

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

    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< }

    问题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;
    }
    
    
    如此一来,这个程序就没有什么问题了。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

    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[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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5.返回对象注意

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

  • 相关阅读:
    基于springboot实现校园医疗保险管理系统【项目源码】计算机毕业设计
    JVM 问题排查-可视化工具
    【每日一题】1450. 在既定时间做作业的学生人数
    [附源码]java毕业设计高校资源共享平台
    Java面试:全面掌握Java知识的捷径!
    [SQL-SERVER:数据库安全及维护]:MSSM工具进行附加还原备份等操作
    使用Python导出hustoj题目提交代码结果(用于收集留言的题目)
    一文学会,三款黑客必备的抓包工具教学
    很多行业在很多人看来门槛很低,人人都可以做
    《算法通关村第二关——指定区间反转问题解析》
  • 原文地址:https://blog.csdn.net/m0_60343477/article/details/126341397