当前位置: 首页 > article >正文

类和对象 - 下

 

      

 本文已收录至《C++语言》专栏!
作者:ARMCSKGT

      


目录

 

前言

正文

初始化列表

成员变量的定义与初始化

初始化列表的使用

变量定义顺序

explicit关键字

隐式类型转换

自定义类型隐式转换 

explicit 限制转换

关于static

static声明类成员

友元

友元函数

友元函数特殊使用场景

友元类

内部类

概述

特性

匿名对象

编译器对于自定义类型的一些优化

隐式类型转换的优化 

传参优化

返回值优化

说明

合理使用优化

关于对象的理解

最后


 

前言

前面我们介绍了类和对象的基本概念以及类的六个默认成员函数,这些知识已经为我们搭起了一个基本的类框架,不过类和对象中还有一些小细节需要介绍,本节我们将进入类和对象的收尾阶段!


正文

初始化列表


成员变量的定义与初始化

上篇我们学习了构造函数,构造函数是用来初始化成员变量的,而成员变量的定义是在初始化列表,对于一些需要在定义时就赋值的成员,例如 const int x ,这时需要使用初始化列表进行,因为在C++11之前,C++98并不支持在声明列表中给缺省值!

  

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

    

int num = 0; //在定义时赋初值 - 初始化
num = 1; //被定义后赋值 - 赋值

那么初始化列表与构造函数有什么关系呢?构造函数是对象在实例时就调用的一个函数,初始化列表在构造函数中,会随构造函数一起执行,初始化列表最先执行并将指定的值赋给每个成员变量!如果我们没有显示去写初始化列表编译器仍然会执行初始化列表,只不过内置类型初始化为随机值,自定义类型则调用其默认构造函数

      

示例1:

class Date 
{
public:
    //猜猜下面两个构造函数的区别
    Date() //初始化列表初始化
        :_year(2008)
        ,_month(12)
        ,_day(21)
    {
        _year = 0;
        _month = 0;
        _day = 0;
    }

    Date(size_t year, size_t month, size_t day) //函数内赋值
    { 
        _year = year;
        _month = month;
        _day = day;
    }

private:
    size_t _year;
    size_t _month;
    size_t _day;
};

int main()
{
    Date d1;
    Date d2(1970,1,1);
    return 0;
}
使用初始化列表的构造函数
未使用初始化列表的构造函数

 示例2:

class Test
{
public:
    Test() //a会被初始化为什么?
        :a(1)
    { }
private:
    int a = 2;
};

int main()
{
    Test t;
    return 0;
}
编译器优先使用初始化列表中的初值进行初始化

初始化列表的使用

   

初始化列表:在函数参数()后,函数体前{},以一个(:)冒号开始,接着是一个以(,)逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,最后一个列表成员后没有任何符号。

//1.所有对象的成员变量都是在初始化列表被定义的
//2.无论是否显示在初始化列表写,每个变量都会在初始化列表中被定义和初始化

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
        :_year(year) //初始化列表初始化
        , _month(month)
        , _day(day)
    {
        //其他初始化行为    
    }
private:
    int _year;
    int _month;
    int _day;
};

   

注意

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量,const成员变量,自定义类型成员且该类没有默认构造函数时(简而言之就是需要在初始化指定初始值的成员变量)

   

//错误示例
class Test2
{
    Test2(int a) {}
};

class Test
{
public:
    Test() {}
private:
    const int num; //这三种成员变量不使用初始化列表初始化是无法编译的!
    int& ref;
    Test2 t;
};

int main()
{
    Test t;
    return 0;
}
上述代码编译报错

   

因为构造函数中函数体是以赋值的方式初始化的,在执行赋值语句前,变量需要被定义和初始化,编译器默认的初始化手段是赋随机值,const只能在初始化阶段指定初始值,如果const常亮被赋予随机值是不合理的,这里也突出了构造函数的一些缺陷,为了补足这些缺陷,C++使用初始化列表定义和初始化且初始化列表存在与构造函数中!

       

节选自C++之父Scott Meyers名书Effective C++ ( 其中成员初值列就是初始化列表)

所以尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化!


变量定义顺序

  

成员变量既然在初始化列表定义,那定义的顺序是由什么觉得的?

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关!

class Test
{
public:
    Test() //猜猜a会被初始化为1吗?
        : b(1)
        , a(b)
    {
        cout << a << endl;
    }
private:
    int a = 0;
    int b = 0;
};

int main()
{
    Test t;
    return 0;
}
a在b前被定义但是b此时未被初始化所以是随机值

结论:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

 

所以这里建议:初始化列表顺序与成员变量声明顺序一致


explicit关键字


我们在平时写代码中不妨会涉及类型转换,类的实例化也是,实例化对象时也会发生类型转换!


隐式类型转换

对于两个类型不同的变量,如果要赋值则需要进行类型转换。

int i = 0;
double d = 1.23456;
i = d; //这里会发生隐式类型转换


自定义类型隐式转换 

//示例代码
class Test 
{
public:
    //默认构造函数
    Test(int num = 0)
        :_num(num)
    {
        cout << "构造函数" << endl;
    }

    //拷贝构造
    Test(const Test& t)
    {
        _num = t._num;
        cout << "拷贝构造" << endl;
    }

    //赋值重载
    Test& operator=(const Test& t)
    {
        _num = t._num;
        cout << "赋值重载" << endl;
    }

private:
    int _num;
};

int main()
{
    //初始化会成功吗?
    Test t = 10;
    return 0;
}

很显然,初始化成功了,但是我想告诉大家,这次初始化调用了构造函数和拷贝构造和两个函数,进行了隐式类型转换然后初始化了 t 对象! 

实际运行调用

这里大家可能会怀疑,上面解释的明明会调用两个构造函数,但是实际上却只调用了一个构造函数,这里要说明的是:编译器为了优化效率,将10直接对t进行构造(直接构造),这样可以提高效率!

Test t = 10; //这样效率太低了

Test t(10); //编译器优化为直接构造

    

当然如果类初始化时有多参数参与初始化,也支持多参数构造优化!

class Test
{
public:
	Test(int a, int b)
		: _a(a)
		, _b(b)
	{
		cout << "Test()" << endl;
		cout << "_a = " << _a << " " << "_b = " << _b << endl;
	}
private:
	int _a = 0;
	int _b = 0;
};

int main()
{
	Test t = { 1,2 };
	return 0;
}


explicit 限制转换

   

铺垫了这么久,重点来了!

有时候我们不想使构造函数方式类型转换进行构造,可以在构造函数前加上 explicit 关键字禁止类型转换

//示例代码
class Test 
{
public:
    //默认构造函数 - explicit修饰限制类型转换行为
    explicit Test(int num = 0)
        :_num(num)
    {
        cout << "构造函数" << endl;
    }

    //拷贝构造
    Test(const Test& t)
    {
        _num = t._num;
        cout << "拷贝构造" << endl;
    }

    //赋值重载
    Test& operator=(const Test& t)
    {
        _num = t._num;
        cout << "赋值重载" << endl;
    }

private:
    int _num;
};

int main()
{
    //初始化会成功吗?
    Test t = 10;
    return 0;
}
此时类型转换也不存在了,编译器优化也就不存在了

所以,如果我们想要提高代码的可读和规范性,必要时使用explicit修饰构造函数就能防止实例化时发生类型转换!


关于static


在C语言的学习中,我们知道static是静态修饰,被修饰的变量生命周期增长至程序运行周期!但是我们平时在写代码时static必须得慎用,因为其中有很多细节需要我们考虑!


static声明类成员

   

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

  

特性

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
  • 静态成员变量必须在类外定义和初始化且只能初始化一次,定义时不添加static关键字,类中只是声明
  • 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员(这样就不需要实例化对象,只需要类名就能访问)。
  • 静态成员函数只能访问静态成员,因为没有this指针无法调用其他非静态成员,静态成员函数是为了静态成员变量而生的
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制。

   

//示例
class Test
{
public:
	//Test()
	//	:a(0) 
	//{}
//不能在初始化列表初始化,因为静态成员a不属于任何一个单独的对象,a被所有对象共享
	static int a; //声明为静态成员
};

int Test::a = 0; //类外定义和初始化静态成员 - 类型 类名::静态成员

int main()
{
	++Test::a; //通过类名 Test::a 访问静态成员
	cout << "Test::a : " << Test::a << endl;
	Test t;
	++t.a; //通过对象 t.a 访问静态成员
	cout << "t.a : " << Test::a << endl;

	return 0;
}

  

重点总结: 静态成员不能调用非静态成员,因为没有this指针,但是非静态成员可以调用静态成员,因为静态成员具有全局属性!


由静态成员的特性,可以求出实例化了多少个对象!

因为实例化对象每次都会调用构造函数,我们可以定义一个静态成员计数器num,每次调用构造函数num就加1,最后就可以完成对构造函数调用次数的统计!


class Test
{
public:
	Test() //默认构造
	{
		cout << "Test()  ";
		++num;
	}

	~Test() //析构函数
	{
		cout << "~Test() ";
		cout << "num = " << num << endl; //打印当前num
	}

	static int num;
};

int Test::num = 0;

int main()
{
	for (int i = 0; i < 10; ++i) //循环进行实例对象
	{
		Test t;
	}
	return 0;
}


友元


概述:有些场景下,某些外部函数需要访问类私有和保护成员,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

 

友元分为:友元函数和友元类

 

友元声明关键字: friend


友元函数

 

将函数使用friend修饰并声明在类中,则此函数称为该类的友元函数!

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明。

//区别示例
class Test
{
    friend void fun1(const Test& t); //声明为友元函数
public:
    Test(int n = 0)
        :mynum(n)
    {}

private:
    int mynum;
};

void fun1(const Test& t)
{
    cout << t.mynum << endl; //友元函数可以直接访问对象的内部私有和保护成员
}

void fun2(const Test& t)
{
    cout << t.mynum << endl; //这里会报错
}//其他函数只能访问公开成员

   

特性

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

友元函数特殊使用场景

 

自定义类型重载流提取和流插入运算符

问题:当我们尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

//日期类重载流插入提取运算符 - 友元函数法
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1970, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}

友元类

 

使用 friend 修饰类并声明在其他类中可以成为该类的友元类!

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

特性

  • 友元关系是单向的,不具有交换性(比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行)
  • 友元关系不能传递
  • 如果C是B的友元, B是A的友元,则不能说明C时A的友元
  • 友元关系不能继承
  • 友元类可以在类定义的任何地方声明,不受类访问限定符限制

内部类


概述

 

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。内部类就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元

class A
{
public:
  class B //B称为A的内部类
  {
   private:
        int Bnum;
  }; 

private:
     int Anum;
};

这里B是A的内部类(友元类),A是B的外部类!B可以访问A类的所有成员,但是A不能访问B类的所有成员!


特性

  • 内部类可以定义在外部类的public、protected、private都是可以的
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
  • 外部类的字节大小与内部类没有任何关系,两者相互独立
  • 与友元类的区别:内部类能受到外类的访问限定符限制
  • 内部类可以访问外类中的成员,而外类无法访问内部类的成员

内部类是平时使用极少,一般用于隐藏类!


匿名对象


class Test
{
    Test(int n = 0)
    :num(n)
    {    
        cout<<"Test()"<<endl;
    }

    ~Test()
    {    
        cout<<"~Test()"<<endl;
    }
private
    int num;
};


int main()
{
    Test(); //构造匿名对象
    return 0;
}

通过 类名() 的方式可以构造匿名对象,匿名对象最大的特性就是生命周期只在一行,对于一些对象只用一次的情况,可以使用匿名对象优化性能。

    

匿名对象的生命周期在某些条件下会延长,例如被const引用变量引用后,其生命周期延长至函数执行结束!


编译器对于自定义类型的一些优化


编译器在实例化,传参和隐式类型转换时并非按正常的流程运行,而是进行了一些优化(简化了执行流程),从而提升执行效率!


//示例代码 - 接下来的测试以此类进行
class Test
{
public:
    Test(int n = 0) //构造函数
    :num(n)
    {
        cout<<"Test(int n = 0)"<<endl;
    }

    Test(const Test& t) //拷贝构造函数
    {
        num = t.num;
        cout<<"Test(const Test& t)"<<endl;
    }

    Test& operator=(const Test& t) //赋值重载函数
    {
        num = t.num;
        cout<<"operator="<<endl;
        return *this;
    }

private:
    int num;
};

隐式类型转换的优化 

  

int main()
{
    Test t = 10;
    return 0;
}

优化前:int类型转换为Test类型都临时对象,拷贝构造临时对象构造对象 t .

//相当于
//10 -> Test tmp(10) -> Test t(tmp)
// 先方式类型转换     再拷贝构造

优化后: 直接将10作为构造参数构造对象 t . 

//相当于
Test t(10);

传参优化

    

void fun(Test t) {}

int main()
{
    fun(10);
    return 0;
}

优化前:int类型转换为Test类型(与以上隐式类型转换相同),然后 t 通过拷贝构造形成对象.

//相当于
//10 -> Test tmp(10) -> Test t(tmp)
// 先方式类型转换     再拷贝构造

优化后:直接构造形成对象.

这里与隐式类型转换的优化相似!


返回值优化

    

Test fun()
{
    return Test(10); //匿名对象
}

int main()
{    
    Test t = fun();
    return 0;
}

优化前:构造匿名对象,返回匿名对象的拷贝临时变量,将临时对象拷贝构造形成对象 t.

优化后:通过返回的匿名对象,直接构造对象 t .


说明

  • 引用传参时,编译器无需优化,因为不会涉及拷贝构造
  • 实际编码时,如果能采用匿名构造,就用匿名构造,会加速编译器的优化
  • 接收参数时,如果分成两行(先定义、再接收),编译器无法优化,效率会降低

编译器会将不必要的 拷贝构造和构造 步骤去掉,优化为直接构造!

  

编译器只能在一行语句内进行优化,如果将这些语句拆分为多条语句,编译器则不会优化,因为编译器不能擅自主张。

//例如
int main()
{
    Test t;
    t = 10;
    return 0;
}


合理使用优化

  

因为编译器对这些情况有优化,所以我们使用以下技巧可以提高程序效率!

  • 接收返回值对象时,尽量拷贝构造方式接收,不要赋值接收
  • 函数返回时,尽量返回匿名对象
  • 函数参数尽量使用 const& 参数

关于对象的理解


现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程。
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。


最后

<类和对象 - 下> 的知识介绍到这里就结束了!本篇介绍了类变量的定义在初始化列表以及初始化列表的合理使用,static成员的定义,友元概念和编译器的一些优化,相信这些细节的加持下,我们能加深对类和对象的理解,更加合理的使用类,类和对象的知识。类和对象的知识介绍到这里就结束了,希望小伙伴们都能理解!

本次 <C++类和对象 - 下> 就介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!

🌟其他文章阅读推荐🌟

C++ <类和对象 - 中> -CSDN博客 

C++ <类和对象 - 上> -CSDN博客

C++入门知识 -CSDN博客

🌹欢迎读者多多浏览多多支持!🌹


http://www.kler.cn/a/2925.html

相关文章:

  • AppAgent 源码 (xml 解析)
  • vulnhub靶场-matrix-breakout-2-morpheus攻略(截止至获取shell)
  • 什么样的LabVIEW控制算自动控制?
  • springBoot发布https服务及调用
  • lxml提取某个外层标签里的所有文本
  • 【时间之外】IT人求职和创业应知【74】-运维机器人
  • 第二十一天 数据库开发-MySQL
  • 大文件上传
  • forward函数——浅学深度学习框架中的forward
  • CVPR 2023 | 旷视研究院入选论文亮点解读
  • HCIP-6.2NAT协议原理与配置
  • Qt5.12实战之控件设计
  • 并查集、并查集+离线、并查集+倒叙回答
  • JVM知识整理
  • Python实现人脸识别检测, 对美女主播照片进行评分排名
  • 串口通信(STM32演示实现)
  • C++ 八股文(简单面试题)
  • 奇安信_防火墙部署_透明桥模式
  • ​selenium+python做web端自动化测试框架与实例详解教程​
  • 数据结构——二叉树与堆
  • 从 X 入门Pytorch——BN、LN、IN、GN 四种归一化层的代码使用和原理
  • 【docker】docker安装MySQL
  • leetcode每日一题:134. 加油站
  • 银河麒麟v10sp2安装nginx
  • [ 网络 ] 应用层协议 —— HTTP协议
  • Linux防火墙——SNAT、DNAT