类和对象(下)

在这里插入图片描述

类和对象(下)


初始化列表

初始化列表是构造函数的一种方式,使用初始化列表可以解决一些构造函数体赋值难以解决的问题

首先先再次认识一下构造函数

构造函数

1.构造函数体赋值

构造函数体赋值就是,我们先前最为常用的方法

class Date {
public:
	Date(int year, int month, int day)//只是在函数体中赋值
	{
		_year = year;
		_month = month;  //{} 这是函数体
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d(2023, 5, 12);//我们要知道的是,调用构造函数之后,对象d有了一个初始值,但是不能成为对对象成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
	return 0;
}

2.初始化列表

初始化列表:以冒号开始,接着是以逗号分割的数据成员列表,每一个成员变量后面跟一个放在括号中的初始值或表达式。

class Date {
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d(2023, 5, 12);
	return 0;
}

注意

1.每一个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2.构造函数体赋值,使得对象进行了赋初值,但是不能成为初始化(初始化只能一次,但是函数体里面可以多次赋值)

3. 类中包含以下成员,必须放在初始化列表位置进行初始化:

1.引用成员变量

2.const成员变量

3.自定义类型成员(没有默认构造函数时)

对于上述三种类型,我们只能使用初始化列表来进行初始化

class Date {
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
		,a(year)
		,b(year)
		,c(year)
	{}
private:
	int _year;
	int _month;
	int _day;
	int& a;
	const int b;
	B c;
};
class B {
public:
	B(int data) {
		_data = data;
	}
private:
	int _data;
};
int main()
{
	Date d(2023, 5, 12);
	return 0;
}

在这里插入图片描述

对于引用类型,和const类型成员变量,共同特点是只能初始化一次,所以不能放在构造函数体中,只能使用初始化列表来初始化变量

对于自定义类型成员(无默认构造),是因为,当有默认构造的时候,编译器会自动调用自定义类型的默认构造,但是当我们重载构造函数之后,就需要我们手动对于自定义类型进行初始化,(也是只能一次)所以只能在列表中初始化

总之,在这以后,我们尽量多去使用初始化列表来进行初始化,因为初始化列表好比是构造函数体初始化的Plus版本,对于一般类型都能进行初始化,而且可以对于引用类型,const类型、自定义类型都能进行初始化(构造函数体不能实现)

成员变量在类中声明的次序就是其在初始化列表中的初始化次序,与其在初始化列表中的先后次序无关
在这里插入图片描述

所以以后写初始化列表的时候,列表顺序保持和声明顺序一致

explicit关键字

在使用构造函数实例化对象的时候,是可以可以通过隐式转换来实例化的

构造函数对于单个参数或者是出去第一个参数无默认值其余均有默认值的构造哈桑怒胡,可以有类型转换的作用(隐式转换)

//隐式转换
class Date {
public:
	//Date(int date)
	//{
	//	_date = date;
	//}
	void Print()
	{
		cout << _date <<" " <<_a<<" " <<_b<< endl;
	}
	Date(int date,int a = 2,int b = 3)
		:_date(date)
		,_a(a)
		,_b(b)
	{}
private:
	int _date;
	int _a;
	int _b;
};

int main()
{
	Date d1(1);//这一种是我们常用的实例化对象的方式
	Date d2 = 1;//这个就是涉及到隐式转换   将int类型转换成Date类型,又因为是构造函数是单个参数
	d1.Print();
	d2.Print();
	return 0;
}

在这里插入图片描述

这一种隐式转换,是对于自定义类型的隐式转换,内置类型的隐式转换在C语言中已经讲解了,所以我们只需要知道自定义类型隐式转换的条件即可

自定义类型隐式转换流程

将内置类型,如本文的int类型转换成Date类型,调用Date构造函数,创建一个临时对象,然后再去调用拷贝构造函数去赋值给d2,按理说,我们应该知道的是,隐式转换是在一条语句中连续两次调用构造函数

class Date {
public:
	//Date(int date)
	//{
	//	_date = date;
	//}
	void Print()
	{
		cout << _date << " " << _a << " " << _b << endl;
	}
	Date(int date, int a = 2, int b = 3)
		:_date(date)
		, _a(a)
		, _b(b)
	{
		cout << "构造函数" << endl;
	}
	Date(const Date& d) {
		cout << "拷贝构造函数" << endl;
	}
private:
	int _date;
	int _a;
	int _b;
};

int main()
{
	Date d1(1);//这一种是我们常用的实例化对象的方式
	Date d2 = 1;//这个就是涉及到隐式转换   将int类型转换成Date类型,又因为是构造函数是单个参数
	d1.Print();
	d2.Print();
	return 0;
}

在这里插入图片描述

对于上述的隐式转换,如果我们不需要这样,避免自定义类型隐式转换,那就使用关键字explicit

使用explicit关键字修饰函数,即可禁止类型转换

class Date{
public:
    explicit Date(int year)  //explicit关键字修饰,禁止隐式类型转换
        :_year(year)
        {}
private:
 	int _year;   
};
int main()
{}

隐式类型转换,可读性就不是很好了,所以使用explicit禁止类型转换

static成员

static修饰的成员称为类的静态成员,有静态成员变量or静态成员函数。静态成员变量一定要在类外进行初始化

我们知道自定义类型的初始化方式,使用构造函数的话有两种,第一个是构造函数体初始化,第二个是初始化列表,但是明显不能解决这个static的初始化,因为static修饰的成员变量的生命周期是整个程序

所以解决方法为,使用全局域对于静态变量进行初始化

class Date
{
public:
    Date(int year)
        :_year(year)
    {}
    static int get_a()  我们不实例化对象,所以就直接用static修饰,类名调用
    {
        return _a;
    }
private:
    int _year;
    static int _a;//public修饰的静态函数/静态变量是可以通过类名直接访问的,但是如果是private的话,是需要类中的函数来访问,所以我们应该定义返回函数
};
int Date::_a = 0;
int main()
{
    cout <<Date::get_a() << endl;
    return 0;
}

对于静态static关键字,主要就下面几个方面的细节要注意

1.static修饰的变量声明周期是整个程序,如果是类中的成员变量被static修饰,那么需要在类外进行初始化,因为类中构造函数不能满足需求,且类外(全局域)初始化就一次,放在静态区,对象使用的时候可以直接调用(只是声明周期不一样而已)

2.static修饰的成员变量or成员函数,是可以直接使用类名来调用的

3.访问限定符private修饰的static成员变量,可以通过成员函数来访问,返回

class Date{
public:
   static int Print()  { cout<<a<<endl;}
private:
   static int a;
};
int Date::a=0;//类外初始化赋值static变量
int main()
{
   cout<<Date::Print()<<endl;
   return 0;
}

总结

  1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 **类名::静态成员 或者 对象.静态成员 **来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

注意:静态成员函数可以调用静态成员函数,但是不能调用非静态函数,非静态函数两者都能调用
这是因为,static修饰的类方法,是整个类所共有的,不属于任何一个对象,所以就没有this指针(形参中没有this指针),继而无法调用非static修饰的函数(this指针是用来接收当前对象的地址的)
函数都存放在代码段

友元

friend关键字修饰函数,使得这个函数是可以突破类的封装,但是友元会增加耦合度(关联程度),破环了封装,所以友元不能多用

分为:友元函数和友元类

友元函数就是在一个类中对于成员函数加上friend修饰,在类外进行定义

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

在这里插入图片描述

因为在类中成员函数会使得this指针占据第一个位置,对于某些功能如重载<<需要cin在第一个参数位置(与this就会争夺,但是this默认为第一个位置的形参),所以我们考虑到可以不使用成员函数,将重载<<放在全局域,但是这个时候因为private限制导致类外无法访问自定义类型,所以我们推出来友元这个定义,实际上就是一个普通函数,只是在类中声明的普通函数(没有this指针),但是因为在类中所以可以访问私有成员变量

注意:

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

友元类

友元类实际上就是在另一个类中声明的一个类,使得这个友元类的所有(成员变量or成员函数)都可以访问该类的私有成员

//下面是对于友元类的例子
#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
using namespace std;
class A {
public:
	int get() {
		return d._year;
	}
private:
	Date d;//创建Date类中的对象d ,然后就可以通过d来调用Date的私有成员变量了    
};
class Date
{
	friend class A;//声明A为Date的友元类
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	//重载>>运算符
	 friend ostream& operator>>(ostream& out, Date& d);
	//{
	//	
	//}

private:
	int _year;
	int _month;
	int _day;
};

ostream& operator>>(ostream& out, Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
}

int main()
{
	return 0;
}

注意:

  • 友元关系的单向的,不具有交换性,如上述Date不能访问A的私有成员,但是A可以访问Date的私有成员
  • 友元关系是不能传递的
  • 友元关系不能继承,继承的话属于父子类,这个概念后序再讲

实际上友元类,在其中声明Date类成员,就是因为当我们需要使用这个Date类中的成员变量时候,通过friend访问访问私有or保护的成员变量,从而实现类外访问

内部类

内部类:如果一个类定义在另一个类的内部,就叫做内部类,内部类是独立的个体,不属于外部类,也并不能通过外部类的对象去访问内部类的成员,但是内部类是天生的友元类(外部类的友元)

特征:

内部类可以放在外部类的任何地方,不受访问限制符的限制

内部类可以直接访问外部类static成员,不需要外部类的类名or对象

sizeof(外部类)=外部类,内部类不影响外部类的大小

//例子
class A
{
	friend class C;
public:
	void get()
	{}
	class B//内部类  
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK  //可以直接访问静态变量不需要类名or对象
			cout << a.h << endl;//OK
		}
	};
private:
	int h;
	static int k;
};
class C//友元类
{
public:
	void get(C& c)
	{
		a.h;
		a.k;
	}
	int date;
	A a;
};
int A::k = 0;
int main()
{
	return 0;
}

匿名对象

匿名对象的使用对于某些场景的情况下是比较好用的,匿名对象就是直接用类名加上(),如:A(),来使用的,创建这个对象之后,再下一行代码就会析构,所以匿名对象由此得名(不会留下痕迹)

//匿名对象的用法
class A
{
public:
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }

    A(const A& aa)
        :_a(aa._a)
    {
        cout << "A(const A& aa)" << endl;
    }

    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;

        if (this != &aa)
        {
            _a = aa._a;
        }

        return *this;
    }

    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};
int main()
{
	//正常实例化对象
    A a(1);//1调用构造函数,实例化对象a
    
    
    //当只有无参构造的时候(or默认构造函数) 实例化对象应该为 A a;//这样即可  不能加上()
    //A a();  这个代码是错误的,因为编译器无法判断你这个是函数的声明还是实例化对象
    
    //匿名对象的创建
    A(2);//这个就是匿名对象,我们可以发现,在编译器在运行到下一行的时候就会调用析构函数,销毁这个匿名对象
    
    //匿名对象的适合使用的场景,比如在,使用stl的时候,放进去vector容器,使用匿名对象,后序再说

    return 0;    
}

拷贝对象的编译器的优化

前文,我们讲到了,对于同一行的代码连续调用构造函数(or拷贝构造函数),按照我们的理论会正常进行调用这两个函数,但是编译器对于这个做法,进行了优化,使得无论理论上多少次调用,但是经过优化,只需要调用一次构造函数

class A
{
public:
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }

    A(const A& aa)
        :_a(aa._a)
    {
        cout << "A(const A& aa)" << endl;
    }

    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;

        if (this != &aa)
        {
            _a = aa._a;
        }

        return *this;
    }

    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};//我们继续使用这个类的模板

void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
    //非连续使用构造函数
    A a1(1);
    A a2=a1;//我们这样是分开的,不会进行优化
    
    //连续调用
    f1(A(2));//这样的话,理论上我们就会先进行匿名构造,然后传值拷贝构造创建临时对象赋值给aa,所以是连续两次构造,编译器进行优化为1次
    
    //当返回值为自定义类型A 的时候+赋值
    A a3=f2();//先f2的返回值需要调用拷贝构造,返回一个临时对象,然后需要赋值给a3(再次调用拷贝构造)   所以 (aa返回)拷贝构造 + (a3赋值)拷贝构造 = 优化为一次构造(一步到位,临时对象直接给a3)
    return 0;
}

首先我们要认识到赋值是什么,拷贝构造是什么,什么时候是赋值什么时候是拷贝构造

我们知道一点即可,是否已经创建了这个对象

如果早就创建了对象,那么就是赋值

如果没有,那么就是拷贝构造

new创建对象

对于c++、java这样的编程语言的话,那么我们一定知道new一个对象这样的说法,接下来先了解一下new

在C语言中我们去动态申请空间的时候,使用到malloc、calloc等函数,但是过程比较繁琐,需要强制类型转换等,对于C++来讲,我们也能动态申请空间,使用new关键字来实现

#include<malloc.h>
int main()
{
    //创建一个数组,动态申请空间
    int* a1=(int*)malloc(sizeof(int)*10);//这样是C语言的方式
    free(a1);//释放空间
    
    //c++
    int* a2=new int[10];//这样就可以啦
    delete[] a2;//delete关键字来释放空间,要根据类型,这个是new [] 所以释放空间的时候跟着[]
	//new也可以做到初始化
    int* a3=new int(10);//初始化数值为10,但是不能做到calloc那样,对于数组来进行全部的赋值(统一值)
    //int* a4=(int*)calloc(0,sizeof(int)*10)//a4[]数组初始化值都为0
    
    int* a5=new int[5]{1,2,3};//这样对于数组进行初始化是可以的(使用大括号)
    //前三位为1 2 3 后两位为0
    return 0;
}

实际上可以实现数组元素全部为0,如int* pa=new int[10]{};//这样就可以实现了类似于calloc(0,sizeof(int)*10);一样的效果

当然除了内置类型可以new,自定义类型也可以,但是这两个都需要用对应类型的指针的方式来接收,因为new是申请的空间(堆空间),返回的也是地址

//演示样例
class A
{
public:
    A(int a, int b)
        :_a(a)
        , _b(b)
    {}
    void Print()
    {
        cout << _a << " " << _b << endl;
    }
private:
    int _a, _b;
};

int main()
{
    A* a1 = new A(1, 2);//开空间+调用构造函数初始化  这个也进行了构造函数优化
    a1->Print();
    return 0;
}
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小王学代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值