C++初阶---类和对象(类的默认成员函数和其他)

类的默认成员函数和其他

  • 概览
  • ①构造函数
      • 1.概览
      • 2.特性
      • 3.特性分析:
        • 特性4
        • 特性5
        • 特性6
        • 特性8
      • 4.构造函数体赋值
      • 5.初始化列表
        • 注意2
        • 注意3
        • 注意4
        • 总结
      • 6.关键字explicit
  • ②析构函数
      • 1.概览
      • 2.特性
      • 3.特性分析:
        • 特性5
        • 析构函数的调用
  • ③拷贝函数构造
      • 1.概览
      • 2.特性
      • 3.特性分析
        • 特性2
        • 特性3 和 特性4
        • 题目
  • ④赋值运算符重载
      • 1.运算符重载
      • 2.赋值运算符重载
      • 3.赋值运算符特性:
      • 4.特性分析:
        • 特性5
        • 自己实现的意义
      • 5.常见的运算符重载
  • ⑤ 取地址及const取地址操作符重载(两个只需要了解)
      • 取地址操作符重载
      • const取地址操作符重载
  • const(修饰类成员函数)
      • 总结
  • static成员
      • 特性
      • 总结
  • 友元
      • 概览
      • 友元函数
      • 友元类
  • 内部类

概览

任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数(包括但不限于这6个)


类的6个默认成员函数

  1. 构造函数完成初始化工作
  2. 析构函数完成清理工作
  3. 拷贝构造函数使用同类对象初始化创建对象
  4. 赋值操作符重载把一个对象赋给另一个对象
  5. 取地址操作符重载:返回地址
  6. const取地址操作符重载

①构造函数

1.概览

如下代码所示

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1,d2;
	d1.SetDate(2018,5,1);
	Date d2;
	d2.SetDate(2018,7,1);
	return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,过于麻烦
为此引出构造函数:


定义
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器 自动调用保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
注意
构造函数是特殊的成员函数,其主要任务并不是开空间创建对象,而是初始化对象


2.特性

注意默认构造函数(不传参就可以调用的那个函数)


特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载(见下方分析)
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成(见下方分析)
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数(见下方分析)
  7. 默认构造函数对于内置类型调用编译器自己生成的默认构造函数貌似没有用但是对于自定义类型就有用了
  8. 成员变量的命名风格(见下方分析)

3.特性分析:

特性4

构造函数重载

class Date
public :
// 1.无参构造函数
	Date ()
	{}
	// 2.带参构造函数
	Date (int year, int month , int day )
	{
	_year = year ;
	_month = month ;
	_day = day ;
	}
private :
	int _year ;
	int _month ;
	int _day ;
};
int main(){
	Date d1; // 调用无参构造函数
	Date d2 (2015, 1, 1); // 调用带参的构造函数
}

此处注意
只有有参的构造函数时不能无参调用,编译器会报错


特性5

类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(不传参就可以调用的那个函数),一旦用户显式定义编译器将不再生成

class A
{
public:
	A()
	{
	_a1=0;
	_a2=1;
	}
private:
	int _a1;
	int _a2;
};
class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	*/
private:
	int _year;
	int _month;
	int _day;
	A _a;
};
int main()
{
	Date d;// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
}

但是我们可以看到,创建的对象只要没有定义了的构造函数都是随机值
C++初阶---类和对象(类的默认成员函数和其他)_第1张图片
解释
编译器默认生成构造函数的时候

  1. 内置类型不会初始化(内置类型是指 int,char…)
  2. 自定义类型才会调用他的无参构造函数进行初始化
  3. 如果类A中也没有自定义构造函数,同样也会是随机值
  4. 注意 默认生产的构造函数发现类中没有无参构造函数时也会报错(解释的不清)

在c++11中,语法委员会打了一个补丁,如下可以用缺省值进行初始化

class Date
{
pubic:
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	
private:
	int _year=1;
	int _month=1;
	int _day=1;
	A _a=10;//下面会说到隐式类型转换
	int* _p=(int*)malloc(sizeof(int))*10;//可以调用函数做缺省值
	static int _n;//静态成员在这里不能给缺省值,不能在构造函数初始化,参见下面的static成员部分
};

注意此处仍是成员变量定义,像函数给缺省值那样,不是初始化C++初阶---类和对象(类的默认成员函数和其他)_第2张图片


特性6

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个


下面代码会出错

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

解释
编译器会出现歧义,不能明确知道调谁
注意
无参构造函数全缺省构造函数我们没写编译器默认生成的构造函数,都可以认为是默认成员函数


特性8

如下代码

class Date
{
public:
	Date(int year)
	{
	// 这里的year到底是成员变量,还是函数形参?
	year = year;
	}
private:
	int year;
};

解释
创建出来的对象中的year是随机值,局部性原则,因为year = year;这条语句以最近的year为主
如果想要正确初始化应该是this->year=year;


所以我们给类成员变量命名时建议在前面加一个标识符‘_’,如_year

4.构造函数体赋值

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

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

5.初始化列表

在没有接触列表初始化之前,我们想要传参初始化一个自定义类可以这样

class A
{
public:
	A(int a=0)
	{
		_a=a;
	}
private:
	int _a;
};
class B
{
public:
	B(int a,int b)
	{
		// A aa(a);
		// _aa=aa;
		_aa=A(a);//创建一个_a=a的匿名对象来赋值给_aa
		_b=b;
	};
private:
	int _b=1;
	A _aa;
}
int main()
{
	B b(10,20);
	return 0;
}
  1. 创建一个_a=a的匿名对象来赋值给_aa,会调用两次A的构造函数 (因为有自定义类型_aa,调用B的构造函数会先去调用一次A的默认构造函数,第二次 是匿名对象创建调用一次)
  2. 同时还调用了一次默认的拷贝构造函数,用来给_aa传值拷贝(内置类型_a)
    C++初阶---类和对象(类的默认成员函数和其他)_第3张图片
    可以看到初始化_aa的代价较大

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

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

我们将上面的讲到的_aa改为初始化列表初始化会这样:
C++初阶---类和对象(类的默认成员函数和其他)_第4张图片
只调用一次构造函数,效率提高
解释(只调用一次默认构造)
即使没显式的写初始化列表,也会在初始化列表处调用A的构造函数(因为有自定义类型_aa)

注意

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化 (见下面分析)
  3. 类中包含以下成员(引用成员变量,const成员变量自定义类型成员(该类没有默认构造函数)),必须放在初始化列表位置进行初始化(见下面分析)
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

注意2

对以下代码反汇编

public:
	B(int a, int b)
	{
		// A aa(a);
		// _aa=aa;
		_aa = A(a);//创建一个_a=a的匿名对象来赋值给_aa
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};

C++初阶---类和对象(类的默认成员函数和其他)_第5张图片
可以看到在初始化列表处调用A的构造函数


所以
尽管我们没有显式的写初始化列表,这里也是认为有初始化列表的_b和_aa会使用默认初始化列表进行初始化

我们也可以认为初始化列表是对象成员变量定义的地方

注意3

  1. const成员变量:const类型的成员必须在定义的时候初始化
  2. 引用成员变量: 也必须在定义的时候初始化
  3. 自定义类型前面已经说到,当没有默认构造函数的时候只能在定义的时候初始化
class A
{
public:
	A(int a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a,int c, int& ref)
		:_c(c)
		,_ref(ref)
		,_aa(a)
	{
		//_c=c;//const变量定义了就不能被改变
		//_ref=ref;//不行必须在定义的时候初始化
		_ref = 100;//对ref进行修改
	}
private:
	const int _c;
	int& _ref;
	A _aa;
};
int main()
{
	int ref = 10;
	B b(0, 1, ref);
	return 0;
}

注意4

看下面这个例子:

class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}
void Print() {
	cout<<_a1<<" "<<_a2<<endl;
}
private:
	int _a2;
	int _a1;
}
int main() {
	A aa(1);
	aa.Print();
}

结果输出:1,随机值


解释
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中出现的先后次序无关
声明的时候是先声明的_a2,所以会先执行_a2(a1),在执行_a1(a),打印的时候自然是1,随机值
建议
建议声明的顺序尽量和初始化列表保持一致,避免出现上面的问题

总结

  1. 尽量使用初始化列表初始化,因为就算你不显式的用初始化列表,成员也会先用初始化列表初始一遍
  2. 有些成员是必须在初始化列表初始的,(引用,const,没有默认成员函数)的成员
  3. 初始化列表和函数体内初始化,可以混用,互相配合,例如下面代码:
List()
	:_head(BuyNode(0))
{
	//有些用初始化列表不能完成初始化,还是要用函数体内初始化
	_head->next=_head;
	_head->prev=_head;
};

6.关键字explicit

我们先看下面的代码

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout<<"A(int a)"<<endl;
	}
	A(const A& aa)
	{
		cout<<"A(const A& aa)"<<endl;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	//下面的本质是一个隐式类型转换
	A aa2=2;
	return 0;
}

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用类似于double类型强转为int类型,中间会产生临时变量,这里相当于把int类型的2强转为A类,同理会产生临时对象
关于隐式类型转换参考:C++初阶—C++基础入门概览的常引用部分
C++初阶---类和对象(类的默认成员函数和其他)_第6张图片

  1. 对于 A aa1(1); 是直接调用构造函数
  2. 对于 A aa2=2; 相当于是用一个整形变量给A类型对象aa2赋值,实际编译器背后会用2构造一个无名对象(临时),最后用无名对象(临时)给aa2对象进行赋值(去拷贝构造aa2),但编译器对其进行了优化,结果和 A aa1(1);一样了
    参考(编译器优化):题目–拷贝构造,析构,静态成员变量…的题目1

explicit关键字 用explicit修饰构造函数,将会禁止单参构造函数的隐式转换
如图报错,验证了上面是编译器优化的结果,本质上其实是类型转换

C++初阶---类和对象(类的默认成员函数和其他)_第7张图片


上面探讨的是单参数构造函数(C++98不支持多参数隐式类型转换),
所以当我们使用的是多参数构造函数时(C++11支持)这样:

A aa2={12};

在这里插入图片描述

同理 explicit关键字可以禁止多参构造函数的隐式转换


②析构函数

1.概览

我们知道一个对象是由构造函数进行初始化的,那么析构函数就是清理空间
定义
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数是特殊的成员函数


2.特性

特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数

3.特性分析:

特性5

如下代码

class Stack
{
public:
	Stack (int capacity = 10)
	{
		_a = (int*)malloc(capacity * sizeof(int));
		_capacity = capacity;
	}
	~Stack()
	{
	free(_a);
	_a=nullptr;
	_top=_capacity=0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class SeqList
{
public :
	SeqList (int capacity = 10)
	{
		_pData = (int*)malloc(capacity * sizeof(int));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	/*~SeqList()
	{
		if (_pData)
		{
			free(_pData ); // 释放堆上的空间
			_pData = NULL; // 将指针置为空
			_capacity = 0;
			_size = 0;
		}
	}*/
private :
	int* _pData ;
	size_t _size;
	size_t _capacity;
	Stack st;
};

C++初阶---类和对象(类的默认成员函数和其他)_第8张图片
C++初阶---类和对象(类的默认成员函数和其他)_第9张图片
可以看到编译器自己生成的默认析构函数同构造函数一样,不会处理内置类型成员,自定义类型成员会去调用他的析构函数

析构函数的调用

类的析构函数调用完全按照构造函数调用的相反顺序进行调用

  1. 全局对象先于局部对象进行构造
  2. 静态对象先于普通对象进行构造

③拷贝函数构造

1.概览

在创建对象时,可以创建一个与一个对象一某一样的新对象
定义
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

2.特性

特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们仍需要自己实现,像Stack这种类,不能使用字节序拷贝

3.特性分析

特性2

拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
(编译器会强制检查终止程序)


假设我们的拷贝构造函数是这样的:
(Date d=d2也是拷贝构造语法)

Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

生成默认拷贝构造函数无穷递归C++初阶---类和对象(类的默认成员函数和其他)_第10张图片


所以我们使用引用传参
同时为了防止出现d._day = _day;的情况,不是输出型函数用const进行保护

Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	//d._day = _day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

特性3 和 特性4

在编译器生成默认拷贝构造函数时和之前的构造函数·析构函数不同
拷贝构造函数不会区分内置类型和自定义类型成员

  1. 对于内置类型成员拷贝构造函数对象按内存存储按字节序完成拷贝,为浅拷贝或值拷贝
  2. 对于自定义类型,会去调用他的拷贝构造函数进行构造

如下代码:
对于Stack这种类必须自己写拷贝构造函数

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{// 像Stack这样的类,对象中的资源需要清理工作,就用析构函数
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

注意到st1和st2中_a的地址是一样的,说明是字节序拷贝(传值拷贝)
C++初阶---类和对象(类的默认成员函数和其他)_第11张图片
解释分析

  1. _a是int类型指针,属于内置类型成员,将st1中的内容按字节序拷贝到st2,st1的指针_a和st2的指针_a都是一样的内容,指向一块空间
  2. 在出了作用域后,会调用析构函数,free掉_a,且两个_a指向同一块地址,造成两次free,同一块空间不能释放两次,程序崩溃
  3. 这里注意,这里是在栈帧上,先进后出的原则,所以先free掉的是st2的_a

题目

参见:题目–拷贝构造的次数


④赋值运算符重载

1.运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
注意

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
  5. 操作符有一个默认的形参this,限定为第一个形参
  6. .* :: sizeof ?: . 注意以上5个运算符不能重载

定义在类外面的运算符重载函数

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year;
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

调用此函数时直接d1==d2;就行,(默认d1是左操作数,d2是右操作数)
编译器会转换为d1.operator==(d2);
再转换为d1.operator==(&d1,d2);

(当然我们也可以显式的写为编译器转换后的语句)


定义在类里面(我们隐式的用this指针)

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& x)// bool operator==(Date* this, const Date& x)
	{
		return _year == x._year    //return this->_year == x._year
			&& _month == x._month  //&& this->_month == x._month
			&& _day == x._day;     //&& this->_day == x._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

d1==d2;(默认d1左操作数对应this指针参数,d2对应右操作数x)


2.赋值运算符重载

下面我们用赋值运算符("=")重载进一步了解他的用法

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2021, 10, 11);
	Date d2(2021, 11, 2);
	Date d3(2020, 12, 2);
	
	d1 = d3;//运算符重载
	
	Date d4(d3);//拷贝构造
	Date d5 = d3;//拷贝构造

	return 0;
}

注意

  1. 运算符重载是用于两个已经定义出来的对象之间的复制拷贝
  2. 拷贝构造是准备定义一个新对象,用另一个对象来初始化它

我们先来看C语言中的’='运算符有这种用法:

int a=1;
int b=4;
int c=10;
a=b=c;

连等操作是从右到左进行的先把c赋给b再把b赋给a,
在这里赋值表达式是需要返回值的,这样才可以把b作为返回值给到a
对于运算符赋值重载也是一样的 应该这样:

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

这里应该是传引用返回 详见赋值运算符重载函数 返回引用 和 返回对象


3.赋值运算符特性:

特性

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝

4.特性分析:

特性5

如果我们没有自定义赋值运算符重载函数,编译器默认生成的赋值运算符重载
与拷贝构造类似,内置类型成员会完成值拷贝自定义类型成员会去调用他的赋值重载

自己实现的意义

对于Stack这种构造函数,析构函数,拷贝构造,赋值重载都需要我们自己写的(前面有提到过为什么)
对于数据结构的类(数据存放在堆上)用C++写基本上都不能用类的默认函数


5.常见的运算符重载

用Date类实现各种运算符重载:前++ 后++ == += -=
参见博客:用Date类实现常用运算符重载


⑤ 取地址及const取地址操作符重载(两个只需要了解)

取地址操作符重载

一般使用编译器默认生成的

Date* operator&()
{
	return this;
}

C++初阶---类和对象(类的默认成员函数和其他)_第12张图片

const取地址操作符重载

针对const对象

const Date* operator&() const
{
	return this;
}

C++初阶---类和对象(类的默认成员函数和其他)_第13张图片

当我们不想让别人使用的时候可以这样(返回空指针而不是this)

Date* operator&()
{
	return nullptr;
}
const Date* operator&() const
{
	return nullptr;
}

C++初阶---类和对象(类的默认成员函数和其他)_第14张图片


const(修饰类成员函数)

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改


(下面注释部分为编译器对const成员函数做的处理)

void Print() const 
//void Print(const Date *this)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
		//cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}

分析

const Date d2(2020, 11, 3);
void Date::Print(Date* this);
d2.Print();//d2.Print(&d2);

这里&d相当于const Date*作为参数传到Print(),但是函数的参数权限是可读可写,相当于放大权限(只可缩小权限不可以放大权限)
所以我们应该这样:

void Date::Print(const Date* this)

也就是:

void Date::Print(Date* this) const

总结

  1. 成员函数加const是有好处的,变成const成员函数以后,const对象可以调用,非const对象也可以调用
  2. 不是所有的成员函数都要加const,如果成员函数是修改型就不能加const(operator+=() ...),
    只读型就应该加(Print() operator+() Size() ...)

static成员

引入(题目)
实现一个类,计算程序中创建出了多少个类对象(构造和拷贝构造的次数)

  1. 思路1:定义一个全局变量count,在调用构造和拷贝构造函数的时候count++
    缺点:全局变量可随意改变,若不小心改变了count 结果出错
  2. 思路2 在类里定义一个私有的静态成员变量count,此类 实例化出来的对象都可以使用此成员变量
  3. 思路3 在类里定义一个公有的静态成员函数static int GetCount(),用来返回成员变量count
class A
{
public:
	A()
	{
		++_count;
	}
	A(const A& a)
	{
		++_count;
	}
	static int GetCount()//静态成员函数没有this指针,不能
	{
		return _count;
	}
private:
	int _a;				// 存在定义出的对象中,属于某个对象
	static int _count;  // 存在静态区,属于整个类,也属于每个定义出来的对象共享// 跟全局变量比较,他受类域和访问限定符限制,更好体现封装,别人不能轻易修改他
	// 声明
};
int A::_count = 0;// 静态成员变量不能在构造函数初始化,在全局位置定义初始化
A f(A a)
{
	A ret(a);
	return ret;
}
void test()
{
	A a1 = f(A());
	A a2;
	A a3;
	a3 = f(a2);
}
int main()
{
	test();
	cout << sizeof(A) << endl; // sizeof(A)算是A定义出来的对象的大小
	// 思路2:属于整个类,也属于每个定义出来的对象共享
	// A::_count = 10;
	cout << a1._count << endl;
	//当对象a1出了test函数就无了,这里只是说明任何此类实例化的对象都可以直接这样使用静态成员变量
	cout << A::_count << endl;
	//思路3:未加static(在类里定义的不是静态成员函数)
	A ret;
	cout << ret.GetCount() - 1<< endl;//有名对象同时要通过实例化对象来传给this指针
	cout << A().GetCount() - 1<< endl;//匿名对象
	//思路3:加static(在类里定义的是静态成员函数)
	cout << A::GetCount() << endl;//直接通过类域调用,因为static成员函数没有this指针
	cout << A().GetCount()-1 << endl;
	return 0;
}

特性

static成员特性

  1. 静态成员变量为所有类对象所共享,不属于某个具体的对象,放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,如:int A::_count = 0;
    (分析静态成员变量在全局位置定义初始化,因为每实例化一个对象,构造函数就去初始化一次并不合理,static变量只用初始化一次)
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员
    参考:C++初阶—类和对象(入门)的this指针部分
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

两个问题

  1. 静态成员函数可以调用非静态成员函数吗
    不可以无this指针
  2. 非静态成员函数可以调用类的静态成员函数吗
    可以有this指针

总结

static的作用
在C中:

  1. 修饰全局变量和全局函数,改变链接属性,只在当前文件可见,其他文件不可使用
  2. 修饰局部变量,改变生命周期,局部变为全局

在C++中:

  1. C++兼容C中的属性
  2. 修饰成员变量和成员函数,成员变量属于整个类,所有对像共享,成员函数没有this指针

友元

概览

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

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

友元函数

引入友元
<<'运算符重载
cout在C++中是ostream类的对象

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

	void operator<<(ostream& out)
	{
		out << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	// 运算符重载,运算符有几个操作数,重载函数就有几个参数,
	// 如果是两个操作数,左操作数是第一个参数,右操作数是第二个参数
	//d1.operator<<(cout);
	d1 << cout;
}

我们要在类中定义<<运算符重载就只能这样写d1 << cout(this指针永远是第一个形参),不符合流的特性


我们可以选择在全局位置定义运算符重载:
同时为了可以连续输出, 返回值应该是ostream&

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
	return out;
}

但由于输出的是Date类的私有成员,我们就可以用友元
友元可以写在类的任何位置

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
}

这样操作符重载函数可以使用Date类的私有成员

注意

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

友元类

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

  1. 友元关系是单向的,不具有交换性
    比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行
  2. 友元关系不能传递
    如果C是B的友元,B是A的友元,则不能说明C时A的友元

代码如下:

class Date; // 前置声明
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)
	{}
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;
};

内部类

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


代码如下:

class A
{
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	private:
		int _b;
	};
private:
	static int k;
	int h;
};
int A::k = 0;
int main()
{
	cout << sizeof(A) << endl;
	A aa;
	A::B bb;
	bb.foo(aa);
	return 0;
}

注意

  1. 内部类可以定义在外部类的public、protected、private都是可以的
  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
  3. sizeof(外部类)=外部类,和内部类没有任何关系
    (上面cout << sizeof(A) << endl;计算A类型对象大小的时候,不考虑B。因为B作为A的内部类,跟普通类并没有什么区别,只是定义在A的内部,他受到A的类域的限制和访问限定符的限制)

你可能感兴趣的:(C++,c++)