类和对象(下)

目录

一.再谈构造函数

1.1构造函数体赋值引出初始化列表 

1.2初始化列表

1.3什么成员是必须使用初始化列表初始化的 

1.4成员变量在类中声明次序

二. explicit关键字

三.static成员

四.c++11的成员初始化新玩法

五.友元

六.内部类

 概念及特性七.练习题

八.再次理解封装

九.再次理解面向对象


一.再谈构造函数

1.1构造函数体赋值引出初始化列表 

class A
{
public:           //自己写了构造函数
	A(int a = 0)
	{
		_a = a;	
	}
private:
	int _a;
};


class B
{
private:         // 自己不写构造函数
	int _b = 1;
	A _aa;
};
int main()
{
    A a;
	B b;
	return 0;
}

说明:

        对于 B,我们不写构造函数,编译器会默认生成一个构造函数。构造函数对内置类型不处理,对自定义类型会去调用它的默认构造函数处理 (无参的、全缺省的、编译器默认生成的),注意无参的和全缺省的构造函数只能存在一个。

       这里 C++ 有一个不好的处理 —— 内置类型不处理,自定义类型处理。针对这种问题,在 C++11 内 又打了一个补丁 ——内置类型后可以加上一个缺省值,你不初始化它时,它会使用缺省值初始化。这是 C++ 早期设计的缺陷。 

1.2初始化列表

  构造函数体内是对对象和变量的赋值,而不是 初始化。

 //只有构造函数才有 初始化列表。 
class Date{
public:
	Date(int year,int month,int day){
		//注意 :构造函数体内部{ }中,并不是对 变量 或 对象进行初始化的
		// 而是对对象的成员变量进行赋值的
		//因为初始化只能初始化一次,赋值可以多次,在函数体内可以对成员变量赋值很多次,所以,它是赋值,不是初始化。
		_year = year;
		_month = month;
		_day = day;
		r = _year;  //这个并不是初始化,而是赋值。

		_year = year;   
		_month = month;
		_day = day;
		r = _year;  //这个并不是初始化,而是赋值。在这里定义之后,引用还不能使用。所以此处是赋值,不是初始化。
	}
private:
	int _year;
	int _month;
	int _day;
	int& r; //引用类型的变量,定义时必须初始化。
};
// 函数体里面不是初始化,而是赋值。
// 如果r要进行初始化 应该怎么做?

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

class Date{

public:
	// 如果没有人为写出初始化列表,编译器会自动生成一份。对成员变量和成员函数初始化。 
	//两种写法
	//之前我们没写,编译器会自动生成一份,所以内置类型都是随机值。
//第一种写法
	Date(int year, int month, int day) :_year(year), _month(month), _day(day)  //初始化列表只有构造函数才有  比如 构造函数 拷贝构造函数
	{
		//构造函数体中是赋值不是初始化
		//如果要初始化只能在构造函数初始化列表
	}
	   //在构造函数中 初始化 只能初始化一次, 在初始化列表中。     对于内置类型,放在初始化列表 
                         和 函数体里面影响不大,但对于某些(3种)类型,必须放在初始化列表里面。
//第二种写法
	Date(int year, int month, int day) 
		:_year(year)
		, _month(month)   //这里是初始化列表
		, _day(day)    
		//,_day(day)err   //变量只能初始化 一次。
	{
		//这里的赋值。

	}
	//拷贝构造的初始化列表
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
{  }
private:
	int _year;
	int _month;
	int _day;
	
};

int main(){
	Date d1(2020, 3, 22);

	return 0;
}

          可以看到对比函数体内初始化,初始化列表初始化可以提高效率 —— 注意对于内置类型,你使用函数体或初始化列表来初始化没有区别;但是对于自定义类型,使用初始化列表是更具有价值的。

         这里还要注意的是函数体内初始化和初始化列表是可以混着用的。

class Time{
public:
        //Time(int hour=0, int minute=0, int second=0)//全缺省默认构造函数
		Time(int hour, int minute, int second)        // 无参默认构造函数
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{  }
private:
	int _hour;
	int _minute;
	int _second;

};
class Date{

public:
	Date(int year, int month, int day)     // 进入Date类构造函数体之前,自动生成初始化列表 就已经调用了Time 类构造函数对_t进行初始化。

	 //相当于编译器自动把变量的初始化给补了
	 // 	: _year(随机值)
	 //     , _month (随机值)
     //     , _day(随机值)
	 // 	, _t (类的调默认构造函数来调用)如果遇到的是显示定义的非默认构造函数,就报错了! 
                          所以才要我们自己在列表初始化里面自己写,而不要编译器默认调用。
// 所以写类的时候,最好带上初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

{
   cout << "Date(int,int,int)" << endl;  // 进入Date类构造函数体之前,自动生成初始化列表 就已经调用了Time 类构造函数对_t进行初始化。
}
private:
	int _year;
	int _month;
	int _day;
	Time _t;    // 我没写_t的初始化,但是编译器会自定
};

int main(){
	Date d(2020, 3, 20);
	return 0;
}

        如果你没有显示的写出初始化列表,但是编译器会自动给构造函数生成初始化列表,对于成员变量,都会初始化。

对于内置变量赋给随机值初始化。

对于类类型对象 调用它的构造函数初始化。

建议:

能够使用初始化列表初始化成员尽量使用初始化列表初始化,因为就算你不显示用初始化列表,成员也会先用初始化列表 初始化一遍。

1.3什么成员是必须使用初始化列表初始化的 

/那些类型必须用初始化列表
class Time{
public:
	Time(int hour=0, int minute=0, int second=0) //全缺省默认构造函数
	//	Time(int hour, int minute, int second) // 无参默认构造函数
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{  }
private:
	int _hour;
	int _minute;
	int _second;

};

class Date{

	//初始化 列表的作用,就是对所有 的变量进行初始化,你不写_t编译器会自动写进去,并且调用默认无参构造函数。所以我们自己把它_t 放到初始化列表中。给他参数,让他调用全缺省构造函数。因为Time类的构造参数就是全缺省的类型。
	// 我们类里面定义了变量。

public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)   //这里是初始化列表
		, _day(day)
		, _r(_day)  //构造函数初始化列表得写上
		, _c(10)    //构造函数初始化列表得写上
		//, _t(); 我们不写不代表没有,编译器会在初始化时  ,自动生成这条语句放在这里。相当于在调用Time类的无参构造函数。此时Time没有无参构造函数,因为一旦定义,编译器就不会再生成默认构造函数
		,_t(12,8,30) // 因为Time 内是 全缺省构造函数,我们给他3个值 就可以了
	{  }

	//拷贝构造的方式
	Date(const Date& d)
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)
		, _r(d._r) //拷贝构造函数初始化列表里面也得写上
		, _c(d._c)//拷贝构造函数初始化列表得也得写上
		//, _t()我们不写不代表没有,编译器会在初始化时  ,自动生成这条语句放在这里。	相当于在调用Time类的无参构造函数 。此时Time没有无参构造函数,因为一旦定义,编译器就不会再生成默认构造函数
		, _t(d._t)//让他用d对象 内的_t 对象来拷贝
	{}


private:          
	int _year;
	int _month;
	int _day;

	  // 下面这三种必须在初始化列表里面初始化。

	int & _r;     // 如果类变量是 引用类变量,那么必须在初始化列表里面 初始化。
               
	const int _c; // 如果类变量 是 const 类型成员变量,那么必须在初始化列表里面 初始化。  
                     C++里面const 定义的变量 是一个常量。所以定义的时候必须初始化。
                   
                  // Time 类里面如果没有显示写构造参数,就会自动产生默认构造参数
	Time _t;      //  那么就没有默认构造函数既(无参构造和全缺省构造函数)就会出现报错。
                           
	              // 必须对他进行在初始化列表 里面的初始化。
	};
int main(){
	Date d(2020, 3, 20);
	const int a = 10;

	return 0;
}

 注意

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

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

  1、引用成员变量 (引用成员必须在定义的时候初始化)

  2、const 成员变量 (const 类型的成员必须在定义的时候初始化)

  3、自定义类型成员 (该类没有默认构造函数,因为如果自己不初始化,那么编译器默认生成的初始化列表里面,就是调用该对象的默认构造函数,但是它没有,会报错!)

1.4成员变量在类中声明次序

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

class Date{

public:
	Date(int year, int month, int day)
		//:_month(month)   // 2   初始化时,按照类变量在类中的声明次序
		//, _year(_month)  // 1   尽量避免使用成员去初始化成员,因为可能成员还没初始化,再用它 
                                  去初始化 其他变量会出现错误。
		//, _day(_month)   // 3   

		: _year(year)       //按照顺序写
		, _month(month)
		, _day(day)         

	{
		cout << "Date(int,int,int)" << endl;
	}

	void Print()const
	{
		_day += 1;
	}
private:
	    int _year;
	    int _month;
mutable	int _day; // 被mutable修饰的成员变量 可以在const成员函数内部被修改。
	
};

int main(){
	
	Date d(2022,3,22);
	return 0;
}

说明

上面的程序输出 

随机值   3  3

因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。


二. explicit关键字

1.单参构造函数具有类型转换的作用。explicit修饰函数,可以禁止。

//单参构造函数具有类型转换的作用。
class Date{
public:
/*explicit*/  Date(int year)   
		: _year(year)       
	{
		cout << "Date(int)" << this <<endl;
	}

	Date(const Date& d)
		:_year(d._year)	
	{ 
		cout << "Date(const Date&)" << this << endl;
	}
	
	Date& operator=(const Date& d){
		cout << this << "=" << &d << endl;
		if (this != &d){
			_year = d._year;
		}
		return *this;
	}
	~Date(){
		cout << "~Date():" << this << endl;
	}
private:
	int _year;

};

int main(){

	Date d1(2022);    //构造函数
	Date d2 = d1;     // 注意:拷贝构造函数 ,因为此时d2还没创建成功。就相当于  Date d2(d1); 所以是拷贝构造函数 不是赋值函数
	Date d3(2023);    
	d3 = 2024; //2024是int 类型,不能直接给Date类型对象d3来进行赋值,但是编译器编译Date时,发现Date具有一个int 类型的 单参构造函数 
	           //于是编译器就将2024借助单参的构造函数 生成一个临时对象,此处实际赋值是用生成的临时对象对d3进行赋值的,而不是直接用2024赋值的
	           // 当赋值结束,临时对象就被销毁了。
	           //可读性不好! 有时候要禁止这个行为。 用explicit 修饰单参函数 ,这种行为就会报错!
	return 0;
}

 

 2.如果是多参构造函数有默认值的情况 ,那么类型转换也可以实现。 使用explicit也可以禁止。

class Date{

public:
/*explicit*/ Date(int year=0,int month=0 ,int day=0)  //如果是多参数有默认值的情况 ,那么那个d3 = 2024;  也可以实现 。 explicit也可以起到作用。

		: _year(year)       
		, _month(1)
		, _day(1)         

	{
		cout << "Date(int,int,int)" << this <<endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)

	{ 
		cout << "Date(const Date&)" << this << endl;
	}
	
	Date& operator=(const Date& d){
		cout << this << "=" << &d << endl;
		if (this != &d){
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	~Date(){
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day; 

};

int main(){

	Date d1(2022);    //构造函数
	Date d2 = d1;     // 注意:拷贝构造函数 ,因为此时d2还没创建成功。就相当于  Date d2(d1); 所以是拷贝构造函数 不是赋值函数
	Date d3(2023);    
	d3 = 2024; //2024是int 类型,不能直接给Date类型对象d3来进行赋值,但是编译器编译Date时,发现Date具有一个int 类型的 单参构造函数 
	           //于是编译器就将2024借助多参构造函数 生成一个临时对象,此处实际赋值是用生成的临时对象对d3进行赋值的,而不是直接用2024赋值的
	           // 当赋值结束,临时对象就被销毁了。
	           //可读性不好! 有时候要禁止这个行为。 用explicit 修饰多参函数 ,这种行为就会报错!
	return 0;
}

 3. 原理

1. d3 =2024同 Date d3(2023); 这是 C++98 支持的语法,它本质上是一个隐式类型转换 —— 将 int 转换为 Date,为什么 int 能转换成 Date 呢 ? —— 因为它支持一个用 int 参数去初始化 Date 的构造函数。它俩虽然结果是一样的,都是调用构造函数,但是对于编译器而言过程不一样。

Date d3 (2023)直接调用构造函数形成d3。

d3=2024则是调用构造函数形成临时对象,再对d3赋值,形成d3 ,然后临时对象销毁。

2. 

   单参构造函数具有类型转化的作用,可以实现int类型转化Date类型。

   多参构造函数如果它有默认值的情况,也可以实现int类型转化Date的类型。

4拓展

针对于编译器优化、底层机制这类知识可以去了解一下《深度探索C++对象模型》

 如果不想允许这样的隐式类型转换的发生 

这里可以使用关键字 explicit

explicit Date (int year)
	:_year(year)

    {
		cout << "date(int)" << this << endl;
	}

error C2440:无法从 int 转换成 A

5. 多参数隐式类型转换 

class Date
{
public:
	Date(int year, int month)
		:_year(year)
        ,_month(month)
	{
		cout << "A(int,int)" << endl;	
	}

	Date(const Date& a)
	{
		cout << "A(const A& aa)" << endl;	
	}
private:
	int _year;
    int _month;
};

int main()
{
	Date aa(2021, 1);
	//Date aa2 = 2021, 1;//??? 不可以
	Date aa2 = {2021, 1}; //c++11支持
	return 0;
}

说明

Date aa2 = 2021, 1; 错误

明显 C++98 不支持多参数的隐式类型转换,但是 C++11 是支持的 —— Date aa2 = {1, 2}; ,同样编译器依然会优化。

当我们使用 explicit 关键字限制时,它会 error C2440:无法从 initializer-list 转换为 A.


三.static成员

写一个程序,计算程序构造了多少个对象 (构造+拷贝构造) ?

1.使用全局变量统计

int g_count = 0;//全局变量

class Date{

public:
	Date(int year , int month , int day )  //初始化列表里面只能初始化类的成员变量
		: _year(year)
		, _month(month)
		, _day(day)
	
	{
		g_count++;
		cout << "Date(int,int,int)" << this << endl;
	}

	//  构造函数count++
	Date(Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	

	{
		g_count++;
		cout << "Date(const Date&)" << this << endl;
	}

	//赋值重载   并没有创建新对象
	Date& operator=(const Date& d){
		cout << this << "=" << &d << endl;
		if (this != &d){
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	//析构函数count--
	~Date(){
		g_count--;
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

void TestDate(){
	Date d3(2020, 3, 22);
	Date d4(d3);
	//想知道Date目前为止总共创建了多少个对象?
}

int main()
{
	Date d1(2020, 3, 20);
	Date d2(d1);
	TestDate();

	g_count = 0;// 此处使用全局变量 不好,因为在任意位置都可以对他修改访问 ,代码不安全,最终结果不能不正确。
	return 0;
}

说明

全局变量可以帮助我们达到记数的目的,但是有一个问题,g_count 是可以随便改的,这样就很不好,不安全。

优化 

  寻找一个,在类中但和具体对象无关,并且所以对象都可以共享的成员变量。

   静态成员变量



class Date{

public:
	Date(int year , int month , int day )  //初始化列表里面只能初始化类的成员变量
		: _year(year)
		, _month(month)
		, _day(day)
	
	{
		_count++;
		cout << "Date(int,int,int)" << this << endl;
	}

	//  构造函数count++
	Date(Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	

	{
		_count++;
		cout << "Date(const Date&)" << this << endl;
	}

	//赋值重载   并没有创建新对象
	Date& operator=(const Date& d){
		cout << this << "=" << &d << endl;
		if (this != &d){
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	//析构函数count--
	~Date(){
		_count--;
		cout << "~Date():" << this << endl;
	}

//private:
	int _year;
	int _month;
	int _day;
 static int _count ; //静态成员变量 ,在类中只是申明,需要放到类外单独定义。
};

//静态成员变量定义 ,               需要在类外定义,并且不需要再加关键字static.
int Date :: _count =0;


void TestDate(){
	Date d3(2020, 3, 22);
	Date d4(d3);
	//想知道Date目前为止总共创建了多少个对象?
}

int main()
{
	Date d1(2020, 3, 20);
	Date d2(d1);
 cout<<sizeof(Date)<<endl;  //大小只计算类的变量12 静态成员变量不算
 cout << Date::_count <<endl;  //静态变量第一种访问方式
 cout << d1._count <<endl;     //静态变量第二种访问方式
 cout << d2._count <<endl;
 cout << &d1._count <<endl;  //d1 d2访问的地址一样,所以说明静态变量_count 是每个对象共有的。
 cout << &d2._count <<endl;
 
     TestDate();

	return 0;
}

1. 静态成员变量在类中是申明,必须放到类外定义,定义时不加static关键字

2.静态成员没有包含在具体的对象里面,是对象所共享的,不影响sizeof的大小。 

3.静态成员变量的访问  类名::静态成员变量名字  || 对象.静态成员变量名字 ,一般都使用     第一种,就算你写了第二种,他也会自动转化成第一种。

4.静态成员变量不能放在初始化列表的地方,因为 初始化列表只能初始化对象内的成员变         量 但是静态成员变量不属于对象。所以会报错。

5.静态成员变量只能在类外定义时,初始化一次。

6.静态成员变量受访问限定符的限制。

静态成员变量受private的限制,不能访问。所以要寻找一个方法访问private修饰的静态成员变量。

选用类普通成员函数调用对象的静态成员变量 ,需要再对象的基础上才可以调用。选用静态变量函数,不需要创建对象,就可以直接调用比较方便

静态成员函数


class Date{

public:
	Date(int year , int month , int day )  //初始化列表里面只能初始化类的成员变量
		: _year(year)
		, _month(month)
		, _day(day)
	
	{
		_count++;
		cout << "Date(int,int,int)" << this << endl;
	}

	//  构造函数count++
	Date(Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	

	{
		_count++;
		cout << "Date(const Date&)" << this << endl;
	}

	//赋值重载   并没有创建新对象
	Date& operator=(const Date& d){
		cout << this << "=" << &d << endl;
		if (this != &d){
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	//析构函数count--
	~Date(){
		_count--;
		cout << "~Date():" << this << endl;
	}
    //可以通过函数访问_count
   int Getcount(){

    return _count;
  }

   //普通成员函数
    void f1(){}
    //普通成员函数可以调用静态类型函数和变量
     void f1(){
    Getcount();
}
   //静态成员函数
   static void f2(){}
   //const修饰静态成员函数
   //static void f3()const{} //错误const修饰this指针,但是静态成员函数没有this指针。

    // 静态成员函数 
  static int Getcount(){
  //cout<<this<<endl;  //静态成员函数没有隐藏this指针。
  // _day+=1;   //错误 成员变量的使用 其实是 this->_day 但是静态成员函数没有this指针,所以不可以对普通成员变量进行访问。可以访问静态的成员变量。与因为静态成员变量与对象无关。

  // f1();  //静态成员变量里面不可以访问普通成员函数,因为普通成员函数的访问需要this指针,他没有所以不行。
     f2();  //静态成员变量可以实现静态函数的访问,静态函数不属于对象,不需要this指针。
    return _count;
  }
private:
	int _year;
	int _month;
	int _day;
 static int _count ; //静态成员变量 ,不属于任何对象,每个对象都可以访问。
};

//静态成员变量的定义
int Date :: _count =0;


void TestDate(){
	Date d3(2020, 3, 22);
	Date d4(d3);
	//想知道Date目前为止总共创建了多少个对象?
}

int main()
{
// 会出错,普通成员函数必须依靠对象才可以调用。这里还没有对象。
cout<<d1.Getcount<<endl;
	Date d1(2020, 3, 20);
	Date d2(d1);
 cout<<sizeof(Date)<<endl;     //大小只计算类的变量12 静态成员变量不算
 cout << Date::_count <<endl;  //静态变量第一种访问方式
 cout << d1._count <<endl;     //静态变量第二种访问方式
 cout << d2._count <<endl;
 cout << &d1._count <<endl;  //d1 d2访问的地址一样,所以说明静态变量_count 是每个对象共有的。
 cout << &d2._count <<endl;
 
     TestDate();
//普通成员函数的调用需要依考对象
cout<<d1.Getcount<<endl;  

// 静态成员函数不用借助对象直接调用  表明静态函数 属于类,不属于具体对象,所有对象公用。
cout << d.Getcount<<endl;
cout << d2.Getcount <<endl;
cout<< Getcount <<endl;  

	return 0;
}

 静态成员函数只能调用静态类型函数,和静态类型成员变量。

静态成员函数不可以调用普通的成员类型函数 和普通的成员变量。

 

 说明

int _year,int_month,int_day ; 存在定义出的对象中,属于对象。

static int _count; 存在静态区,属于整个类,也属于每个定义出来的对象共享。跟全局变量比较,它受类域和访问限定符限制,更好的体现封装,别人不能轻易修改。

static成员 

对于非 static 成员它们的定义是在初始化列表中,但在 C++ 中,static 静态成员变量是不能在类的内部定义初始化的,这里的内部只是声明。注意这里虽然是私有成员,但是对于 static 成员它支持在外部进行定义,且不需要加上 static,sizeof 在计算的时候并不会计算 static 成员的大小。

_count是私有,怎么访问 ?

1.定义一个公有函数 GetCount 函数,返回 _count, 实例化对象后调用 GetCount 函数并减1。

2.将 GetCount 函数定义成静态成员函数并使用类域调用。

 特性
1. 静态成员变量为所有类对象所共享,不属于某个具体的实例。

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

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

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

5. 静态成员和类的普通成员一样,也有 public、protected、private 3 种访问级别,也可以具有返 回值。

【面试题1】

static 的作用 C 语言 | C++ ?

C 语言:

1、 static 修饰局部变量,改变了局部变量的生命周期 (本质上是改变了变量的存储类型),局部变量由栈区转向静态区,生命周期同全局变量一样。

2、 static 修饰全局变量,使得这个全局变量只能在自己所在的文件内部使用,而普通的全局变量却是整个工程都可以使用。

   为什么全局变量能在其它文件内部使用 ?

    因为全局变量具有外部链接属性;但是被 static 修饰后,就变成了内部链接属性,其它源文件不能链接到这个静态全局变量了

3、static 修饰函数,使得函数只能在自己所在的文件内部使用,本质上 static 是将函数的外部链接属性变成了内部链接属性 (同 static 修饰全局变量)

C++:

1、修饰成员变量和成员函数,成员变量属于整个类,所有对象共享,成员函数没有 this 指针。

【面试题2】

静态成员函数可以调用非静态成员函数吗 ?

不能,因为静态成员函数没有 this 指针。

非静态成员函数可以调用静态成员函数吗 ?

可以,因为非静态成员函数有 this 指针。


 

四.c++11的成员初始化新玩法

class A
{
public:
	A(int a = 0)
		: _a(0)
	{}
private:
	int _a;
};
class B
{
private:
	//缺省值 
	int _b = 0;
	int* p = (int*)malloc(sizeof(int)*10);
	A _aa = A(10);//先构造再拷贝构造,优化为构造
	A _aa = 10;//同上,建议
	//static int _n = 10;//err,静态变量不能给缺省值
};
int main()
{
	B bb;	
	return 0;
}

说明

       C++11 支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值 —— 如果在构造函数中显示给值就会不用缺省值,如果没有显示给,就会用缺省值。这里在上篇文章我们就提到过,这里就写一些上篇文章所没提到过的。

五.友元

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

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

 友元函数

 重载<< 

class Date
{   //加一个friend 告诉类这是一个友元函数。友元函数可以访问对象的私有成员变量。
	friend ostream& operator<<(ostream& out, const Date& d);//友元,位置可任意,一般是开头
	friend istream& operator>>(istream& in, Date& d);//友元
    //友元函数不是类的成员函数
public:
	Date(int year = 0, int month = 0, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
  /* 第一种方式 访问对象的私有成员变量
    void Print(){
    out << _year << "/" << _month << "/" << _day << endl;     
  
} */
  //采用对流插入运算符在类内重载。
  //注意:将operator<< 重载成成员函数后,有一个隐藏的this指针
  // 流插入<<运算符不能重载成成员函数,不符合常规调用。
  //流插入运算符<<重载规定:第一个参数必须是ostream& 第二个参数才是要打印的,现在放成类成员函数,那么编译器就会强制给他一个this指针参数,当做第一个参数。所以顺序反了。
	/*void operator<<(/*Date* const this,*/ostream& out)
	{
		out << _year << "/" << _month << "/" << _day << endl;
	}*/
private:
	int _year;
	int _month;
	int _day;
};


 //全局普通函数
 // 本来不能访问类中的私有成员变量,设置成友元函数之后,就可以了。
//将流插入运算符<<重载成全局函数 , 返回值类型必须是输出流对象的引用---支持连续输出。
ostream& operator<<(ostream& out, const Date& d)//支持连续输出
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
	return out;
}
istream& operator>>(istream& in, Date& d)//支持连续输入
{
	in >> d._year >> d._month >> d._day;
	return in;
}
int main()
{   
    int a =10;
    cout<< a<<endl;   //可以实现

    Date d(2022,4,1);
    cout<< d.Print()<<endl;  //可以借助公用函数实现对象的打印
    cout<< d <<endl;          //失败,因为编译器不知道怎么打印

	Date d1, d2;             
	cin >> d1 >> d2;      //实现不了,因为此时编译器还不知道怎么输入
	cout<< d1 << d2;      //实现不了,因为此时编译器还不知道怎么输出

     cout<< d1;              // 重载为成员函数   报错!
	 d1 << cout;          //  将<<重载成类的成员函数,这里虽然可以通过,但是写的是反的。表达意义不明确。

     cout << d1 ;          //插流入操作符重载成全局函数可以实现,但不支持连续打印
    
	 cout << d1 << d2;      // 当添加返回值后,支持连续打印。
 
    cin >> d1;               //重载流输出运算符之后,这里可以直接使用。
   cout<< d1<< endl;        
	return 0;
}

在 C++ 里 cout 是一个 ostream类 的对象;cin 是一个 istream类 的对象。

cout << d1 错误。

为啥不能 cout << d1 呢 ?

之前说过运算符有几个操作数,重载函数就有几个参数。如果有两个操作数,左操作数是第一个参数,右操作数是第二个参数。

在 Date 类成员函数 operator<< 里对象是第一个参数,因为隐含的 this 指针已经默认占据了,那么 cout 就只能作第二个操作数了。可以倒也可以,但是用起来不符合流运算符原来的特性。只能d1<<cout 这样使用!

怎么 cout << d1 呢 ?

也就是把 cout 作为第一个参数,那么这里就不能用成员函数了,之前我们用成员函数是因为成员变量是私有的。

如何取舍:使用成员函数 | 使用全局函数。这里成员函数的可读性差影响较大,所以将之舍弃,使用全局函数。

支持 cout << d1 后怎么解决私有 ?

解决方案1:提供公有的成员函数 GetYear、GetMonth、GetDay

解决方案2:友元函数

       这里我们就引出了友元,C++ 默认是不能在类外访问私有的,但是它提供了友元以帮助我们解决这种场景的问题。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。

cout << d1 << d2; 

注意与连续赋值大相径同,只是方向相反。

小结

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

2、友元函数不能用 const 修饰,因为 const 修饰的是 this 指针指向的对象。

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

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

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

注意以上部分概念需要与后面的知识结合 ,不懂的可先忽略。


 友元类

class Time
{ 
friend class Date; //声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour, int minute, int second)
 	: _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)
    , _t(17,16,23)
{}
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;
};

分析

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

1.友元关系是单向的,不具有交换性。比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。

2.友元关系不能传递,如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 是 A 的友元。

3. 友元关系不能继承。

六.内部类


概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

注意:

内部类就是外部类的友元类。注意友元类的定义,内部类可以通过内部类的对象参数来访问外部类中的所有成员。但是外部不是内部的友元。

特性

1、内部类可以定义在外部类的 public、protected、private 都是可以的。

2、注意内部类可以直接访问外部类中的 static、枚举成员、不需要外部类的对象/类名。

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

class A
{
private:
	static int k; //静态变量
	int h;
public:
	class B    //B类天生就是A的友元,内部类是外部类的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//ok       
			cout << a.h << endl;//ok	//内部类直接可以访问外部类的私有成员 
		}
	private:
		int _b;
	};
};
int A::k = 0;
int main()
{
	cout << sizeof(A) << endl;//4,计算大小只看内部成员变量不计算内部的类。
    A a;
    //a._b;  //错误不能通过外部类访问内部类成员,内部类不属于外部类
 	A::B b ; //要用B去定义,必须得指定域
	b.foo(A()); 
    return 0;
}

说明

sizeof 在计算 A 类型对象大小的时候,不考虑 B 类。因为 B 作为 A 的内部类,跟普通类没有什么区别,只是定义在 A 的内部,它受到 A 的类域的限制和访问限定符的限制。

一般情况是一个类专门为另一个类服务是才会用到内部类,实际上 C++ 里对于内部类的使用并不多,没有 JAVA 中的频繁。(对于内部类借助下面的例题演示)
 

 概念及特性
七.练习题


1、求1+2+3+…+n<难度系数>
 题述:求 1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C)。

数据范围: 0 < n ≤ 200

进阶: 空间复杂度 O(1) ,时间复杂度 O(n)

示例1:

输入:5

返回值:15

 示例2:

输入:1

返回值:1

核心思想:借助了构造函数和 static 关键字

nowcoder原题

class Sum
{
public:
    Sum()
    {
        _ret += _i;
        _i++; 
    }
    static int GetRet()//在保证封装的情况下访问_ret
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};
//定义
int Sum::_i = 1;
int Sum::_ret = 0;

class Solution 
{
public:
    int Sum_Solution(int n) 
    {
        Sum a[n];//调用n次构造
        return Sum::GetRet();
    }
};

说明

Sum a[n] 就可以调用 n 次构造。但要注意变长数组是 C99 支持的语法,这里能支持是因为 OJ,后面非 C99 要使用可以 Sum* p = new A[n];

改造成内部类如下:

#include<iostream>
using namespace std;
class Solution 
{
private:
	//内部类,为Solution解决1+...+n的问题,更好的体现了封装。
	class Sum
	{
	public:
    	Sum()
    	{
        	_ret += _i;
        	_i++; 
    	}
	};
public:
    int Sum_Solution(int n) 
    {
        Sum a[n];//调用n次构造
        return _ret;
    }
private:
	static int _i;
	static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;

2、计算日期到天数转换<难度系数>
题述:根据输入的日期,计算是这一年的第几天。保证年份为 4 位数且日期合法。

进阶:时间复杂度:O(n)\O(n) ,空间复杂度:O(1)\O(1)

输入描述:输入一行,每行空格分割,分别是年,月,日

输出描述:输出是这一年的第几天

示例1:

输入:2012 12 31

返回值:366

 示例2:

输入:1982 3 4

返回值:63

核心思想:之前我们是把一年中每月的天数存储起来,这里我们存储的是包括当前月份之前的天数。对应的月份 + 天数就是当前默认的天数,再判断闰年。

#include<iostream>
using namespace std;
int main()
{
	//static int monthDays[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31,30,31};
    //改造:
    static int monthDays[13] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
    //2021 10 18
    int year, month, day;
    cin >> year >> month >> day;
    int n = monthDays[month-1] + day;
    //判断2月,注意这里不是等于而是大于
    if(month > 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
    {
        n++;    
    }
    cout << n << endl;
    return 0;
}

3、日期差值<难度系数>
 题述:有两个日期,求两个日期之间的天数,如果两个日期是连续的我们规定他们之间的天数为两天。

输入描述:有多组数据,每组数据有两行,分别表示两个日期,形式为 YYYYMMDD

输出描述:每组数据输出一行,即日期差值

 示例1:

输入:

20110412

20110422

输出:11

 核心思想:

日期 - 日期 

常规思路是日减、月减、年减,补它们中间的位,但是实际上实现较难。

思路一:把某月某日映射到 365 天

思路二 (最优):先比较两日期的大小,然后让小的++,当小的等于大的时,那么加了多少次就是它们相差的天数

4、打印日期<难度系数>

nowcoder原题

5、累加天数<难度系数>

nowcoder原题

八.再次理解封装

C++ 是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。

C++ 通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。

下面举个例子来让大家更好的理解封装性带来的好处,比如:乘火车出行

本质上封装是为了更好的管理:

售票系统,负责售票 —— 用户凭票进入,对号入座

工作人员,售票、咨询、安检、保全、卫生等

火车,带用户到目的地

火车站中所有工作人员配合起来,才能让大家坐车有条不紊的进行,而不需要知道火车的构造,票务系统是如何操作的,只要能正常方便的应用即可。

九.再次理解面向对象

面向对象其实就是在模拟抽象映射现实世界。当然讲到这不是说面向对象已经学透彻了,这里仅仅是入门。

说明:对象,类,就是为了让计算机更好的认识世界。比如一个洗衣机,怎么让计算机认知他?

1.首先明确它的 属性 和 功能

2.通过编程语言将它的属性,功能 形成一个类。

3.类的实例化,形成对象。模拟洗衣机成功。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值