类与对象(下)

深入理解构造函数

构造函数体赋值:

先来看下边一段代码:

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

        _year = 2023;
    }

    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d(1949, 10, 1);
    d.Print();
    return 0;
}

编译运行:

可以发现可以正常运行,且构造函数体内的语句都执行了。

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对成员变量的初始化构造函数体中的语句只能将其称为赋值,而不能称作初始化。因为初始化是在定义变量(给变量开辟内存空间)时给变量赋予一个初始值,每个变量只能初始化一次(因为变量只能定义一次),而构造函数体内可以多次赋值。代码中有两个对_year赋值的语句,能够正常编译运行,就印证了上述描述。

初始化列表:

既然成员变量不是在构造函数体内初始化的,那么到底是在哪里初始化的呢?

答案是  成员变量在初始化列表定义并初始化

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

class Date
{
public:

    Date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {
  
    }

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

1、初始化列表是成员变量实际定义的地方,再使用时会出现以下几种情况:

        (1)没有在初始化列表中显式的写出来,也会在初始化列表中定义,此时内置类型会赋随机值,自定义类型会调用其构造函数

        (2)在初始化列表中写出来了,但是变量后边的括号中没有给定值,此时定义之后变量的值是不确定的(因编译器而异),VS2019下内置类型会被初始化为0,自定义类型会调用其构造函数

        (3)在初始化列表中写出来了,并且变量后边的括号中给定了值,此时内置类型会被定义并初始化为给定的值自定义类型调用其构造函数

class A
{
public:
	A(int aa = 10)
		:_aa(aa)
	{
		cout << "A(int aa = 10)" << endl;
	}

	void Print()
	{
		cout << _aa << endl;
	}
private:
	int _aa;
};

class B
{
public:
	B(int a = 20, int k = 30)
				//_a1没有显式的写出来
		:_a2()  //显式的写出来,但没有给定值
		,_a3(1) //显示的写出来,并给了值
		,_a4(a) //显示的写出来,并给了值
				//_b1没有显式的写出来
		,_b2()  //显式的写出来,但没有给定值
		,_b3(40)//显示的写出来,并给了值
		,_b4(k) //显示的写出来,并给了值
	{

	}
	void Print_A()
	{
		cout << "_a1._aa = ";
		_a1.Print();

		cout << "_a2._aa = ";
		_a2.Print();

		cout << "_a3._aa = ";
		_a3.Print();

		cout << "_a4._aa = ";
		_a4.Print();
	}

	void Print()
	{
		cout << "_b1 = " << _b1 << endl;
		cout << "_b2 = " << _b2 << endl;
		cout << "_b3 = " << _b3 << endl;
		cout << "_b4 = " << _b4 << endl;

	}
private:
	A _a1;
	A _a2;
	A _a3;
	A _a4;

	int _b1;
	int _b2;
	int _b3;
	int _b4;
};

int main()
{
	B b;

	b.Print_A();
	b.Print();
	return 0;
}

运行结果: 

2、初始化列表和普通的构造函数体内赋值是可以混用的,且会按顺序先走初始化列表,再走函数体。

有如下代码:

class Date
{
public:

    Date(int year, int month, int day)
        : _year(year)
        ,_month(2)
    {
        _year = 2023;
    }

    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d(1949, 10, 1);
    d.Print();
    return 0;
}

调试代码,监视_year、_month、_day在程序走完初始化列表后的变化:

最终运行结果:

注意: 

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

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

        引用成员变量;

        const成员变量;

        自定义类型成员(且该类没有默认构造函数时)。

class A
{
public:
	A(int aa)
		:_aa(aa)
	{
	
	}
private:
	int _aa;
};

class B
{
public:
	B(int a, int k)
		:_a(a)
		, _k(k)
		, _n(10)
	{
	
	}
private:
	A _a;	      //没有默认构造函数
	int& _k;	  //引用
	const int _n; //const常量 
};

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

class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int day)
	{
	
	}
private:
	int _day;
	Time _t;
};
int main()
{
	Date d(1);
	return 0;
}

运行结果:

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

class A
{
public:
    A(int a)
        :_a1(a)
        , _a2(_a1)
    {
    
    }

    void Print() 
    {
        cout << "_a1=" << _a1 << "\n" << "_a2=" << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};
int main() 
{
    A aa(1);
    aa.Print();
    return 0;
}

运行结果:

5、声明成员变量时可以给一个值,这个值就是给初始化列表用的

class Date
{
public:
	Date()
	{ }

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day;
};

int main()
{
	Date d;
	d.Print();
	return 0;
}

运行结果:

可以发现构造函数体内没有任何语句,初始化列表也没有写出来,但是类对象的成员变量_year、_month并不是随机值,这就说明了类成员变量在初始化列表定义后,还对_year、_month 成员进行了初始化,而这个初始化的值就来源于声明成员变量时给成员变量给定的值,因为_day定义时没有给定值,所以_day是一个随机值。

explicit关键字

构造函数的类型转换:

1、对于单参数的构造函数,在使用时具有类型转换的作用。

class Date
{
public:
    //单参数构造函数
	Date(int year)
		:_year(year)
	{ 
		cout << "Date(int year)" << endl;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022);
	d1.Print();

	d1 = 2023;
	d1.Print();
	return 0;
}

运行结果:

可以发现,明明只定义了一次类对象,但是却调用了两次构造函数,并且还把一个整型的2023赋给了一个类对象。造成这种结果的原因就是上述代码中出现了隐式的类型转换,用整型的2023先构造一个临时的类对象,再把这个临时的类对象赋给d1。

2、有多个参数的构造函数,当创建对象时可以只传一个参数时(全缺省构造函数、半缺省构造函数),也具有类型转换的作用。

class Date
{
public:

	//半缺省构造函数
	Date(int year, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month = 1, int day = 1)" << endl;
	}

	//全缺省构造函数
	//Date(int year = 2000, int month = 1, int day = 1)

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022);
	d1.Print();

	d1 = 2023;
	d1.Print();
	return 0;
}

运行结果:

结果与单参数的构函数结果相同。

explicit的限制作用:

在构造函数前加上explicit关键字可以禁止构造函数的类型转换作用。

class Date
{
public:

	//有explicit修饰的构造函数
	explicit Date(int year)
		:_year(year)
	{
		cout << "explicit Date(int year)" << endl;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022);
	d1.Print();

	d1 = 2023;
	d1.Print();
	return 0;
}

 编译时会报错:

重载的 “ = ”运算符的两个操作数都是Date类,并且编译报错无法与整型匹配,说明整型的2023没有进行类型转换,说明explicit禁止了构造函数的类型转换作用。

注意:explicit只能禁止隐式的类型转换,无法禁止强制的类型转换

class Date
{
public:

	//有explicit修饰的构造函数
	explicit Date(int year)
		:_year(year)
	{
		cout << "explicit Date(int year)" << endl;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022);
	d1.Print();

	//d1=2023;//隐式类型转换
	d1 = (Date)2023;//强制类型转换
	d1.Print();
	return 0;
}

可以正常编译运行:

static成员

概念:

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

class A
{
public:
	A() 
	{
		++_scount; 
	}
	
	A(const A & t) 
	{
		++_scount; 
	}
	
	~A() 
	{ 
		--_scount; 
	}
	//静态成员函数
	static int Get() 
	{ 
		return _scount; 
	}
private:
	//静态成员变量
	static int _scount;
};

//静态成员变量初始化
int A::_scount = 0;

特性:

1. 静态成员为所有类对象所共享不属于某个具体的对象,存放在静态区。

2. 静态成员变量必须在类外定义定义时不添加static关键字,类中只是声明

3. 类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问。

4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员

5. 静态成员也是类的成员受public、protected、private 访问限定符的限制

class A
{
public:
	A()
	{
		++_scount;
	}

	A(const A& t)
	{
		++_scount;
	}

	~A()
	{
		--_scount;
	}
	//静态成员函数
	static int Get()
	{
		//func();//编译无法通过,静态成员函数没有隐藏的this指针
		return _scount;
	}

	void func()
	{
		cout << "void func()" << endl;
	}
private:
	//静态成员变量
	static int _scount;
};

//静态成员变量初始化
int A::_scount = 0;

int main()
{
	//cout << A::_scount << endl;//编译无法通过,_scount是私有的成员变量
	cout << A::Get() << endl;//静态成员函数可以不借助类对象进行访问

	A a1;
	A a2(a1);
	cout << "a1._scount = " << a1.Get() << endl;
	cout << "a2._scount = " << a2.Get() << endl;

	return 0;
}

运行结果:

a1、a2两个类对象一个调用了构造函数,一个调用了拷贝构造函数,如果静态成员变量时每个类对象独立拥有的,那么a1._scount、a2._scount 的结果应该都是1,但是结果却都是2,这就说明了静态成员变量是所以类对象共有的,不属于某个具体对象。

友元

友元有两种:友元类友元函数

友元函数:

类与对象(中)一文中我们尝试实现了一个简单的日期类的一些简单的操作,但是并没有实现日期类的流插入、流提取,现在我们再来实现一下日期类的流插入、流提取(cout、cin)的重载。

class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {   }

    //流插入
    ostream& operator<<(ostream& _cout)
    {
        _cout << _year << "-" << _month << "-" << _day << endl;
        return _cout;
    }

    //流提取
    istream& operator>>(istream& _cin)
    {
        _cin >> _year >> _month >> _day;
        return _cin;
    }
private:
    int _year;
    int _month;
    int _day;
};


int main()
{
    Date d1;
    cin >> d1;
    cout << d1;
	return 0;
}

正常来说上述代码应该是可以实现对日期类的流插入、流提取操作,接下来尝试编译运行:

编译无法通过,可能有人会认为是对流插入和流提取的实现有问题,那么再来看下边这段代码:

class Date
{
public:
    Date(int year = 1949, int month = 10, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {   }

    //流插入
    ostream& operator<<(ostream& _cout)
    {
        _cout << _year << "-" << _month << "-" << _day << endl;
        return _cout;
    }

    //流提取
    istream& operator>>(istream& _cin)
    {
        _cin >> _year >> _month >> _day;
        return _cin;
    }
private:
    int _year;
    int _month;
    int _day;
};


int main()
{
    Date d1;
    //cin >> d1;
    //cout << d1;
    d1 >> cin;
    d1 << cout;
	return 0;
}

再次尝试编译运行:

这次却可以正常编译运行,那么问题到底在哪里呢?

首先要知道类成员函数在调用时会有一个隐藏的this指针(静态成员函数没有),并且这个隐藏的this指针会默认占据函数参数的第一个位置二元操作符左操作数传参时会占据第一各参数的位置。也就是说流插入、流提取函数的参数实际应为:

    //流插入
    ostream& operator<<(Date* this, ostream& _cout)
    {    }
    //流提取
    istream& operator>>(Date* this, istream& _cin)
    {   }

cin>>d1 、 cout<<d1 的写法实际传参是:

cin >> d1;
operator>>(cin, &d1)
cout << d1;
operator<<(cout, &d1)

所以编译时会报错没有匹配的操作数。

而 d1>>cin 、d1<<cout 的写法实际传参为:

d1 >> cin;
operator>>(&d1, cin);
d1 << cout;
operator<<(&d1, cout);

此时才与类中的函数的参数相对应,所以能够正常编译运行。

但是 d1>>cin 、d1<<cout 的写法太过奇怪,并且类成员函数隐藏的this指针又会默认占据第一个参数的位置,那么该怎么办才能实现正常的流插入、流提取功能呢?有人会说直接定义一个全局的函数,但是全局函数又无法访问类中的私有变量。因此到底该怎么办呢?

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

class Date
{
	//友元函数声明
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, 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 >> d._month >> d._day;
	return _cin;
}

int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

编译运行:

可以发现此时就可以实现正常的流插入流提取操作了。

注意:

1.友元函数可访问类的私有和保护成员,但不是类的成员函数。

2.友元函数不能用const修饰。

3.友元函数可以在类定义的任何地方声明不受类访问限定符限制

4.一个函数可以是多个类的友元函数

5.友元函数的调用与普通函数的调用原理相同。

友元类:

 与友元函数类似,友元类就是在A类中用 friend 关键字声明一个B类,B类定义在A类的外边,B类就是A类的友元类B类中的所有成员函数都是A类的友元函数都可以访问A类中的隐藏信息(包括私有成员和保护成员)。

class Time
{
    //友元类声明
    friend class Date;  
public:
    Time(int hour = 0, int minute = 0, int second = 0)
        : _hour(hour)
        , _minute(minute)
        , _second(second)
    {   }

private:
    //私有成员函数
    void Print()
    {
        cout << _hour << "-" << _minute << "-" << _second << endl;
    }
    //私有成员变量
    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 Set_Time_Date(int hour, int minute, int second)
    {
        // 访问Date类的私有成员变量
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }

    void Print_Time_Date()
    {
        // 访问Date类的私有成员函数
        _t.Print();
    }
private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

注意:

1.友元关系是单向的,不具有交换性:

   比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接 访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

2.友元关系不能传递:

   如果C是B的友元, B是A的友元,则不能说明C是A的友元。

3.友元关系不能继承。

内部类

如果一个类定义在另一个类的内部,这个内部的类就叫做内部类

内部类是一个独立的类, 它不属于外部类,它是一个受外部类的类域访问限制的普通的类不能通过外部类的对象去访问内部类的成员外部类对内部类没有任何优越的访问权限,可以在外部类中使用内部类定义成员

注意:

1.内部类天生就是外部类的友元类。

2.内部类可以不借助 外部类的对象或类名 直接访问外部类的静态成员。

3.sizeof(外部类)和内部类没有任何关系。

class A
{
public:
    // 定义内部类B,B天生就是A的友元类
    class B 
    {
    public:
        void func(A& a)
        {
            cout << _k << endl;
            cout << a._h << endl;
        }
    };

private:
    static int _k;//静态变量
    int _h;
};

int A::_k = 1;

int main()
{
    A::B b;//B在A的类域内

    A a;
    b.func(a);

    cout << sizeof(A) << endl;

    return 0;
}

运行结果:

匿名对象

匿名类即只使用 类名+() 的方法定义匿名对象匿名对象的特点是不用取名字,但是匿名对象的生命周期只有这一行,到下一行就会自动调用析构函数。 

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

int main()
{
    //A a();//错误的定义对象方法,无法与函数声明区分
    A();//匿名对象
    return 0;
}

调试代码:

可以发现刚运行至return语句时,就已经调用的析构函数。

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值