C++基础语法——类和对象

目录

1.类是什么?

2.类的访问限定符

5.类的作用域

6.类的实例化

7.类的对象

8.this指针

9.类的默认成员函数

1.构造函数

①定义

②特征

③结论

④初始化列表

⑤explicit关键字

2.析构函数

①定义

②特性

③适用场景

3.拷贝复制函数

①定义

②特征

4.赋值运算符重载

①运算符重载

②赋值运算符重载

③前置++与后置++的重载

④取地址及const取地址操作符重载

10.const成员

 11.static成员

①概念

②特性

12.友元

①友元函数

②友元类

13.内部类 

①概念

②特性

14.匿名对象

①概念

②特性


1.类是什么?

类(Class)是一种用户自定义的数据类型,它是一组数据和方法的集合,用来描述某个对象的属性和行为。

class className
{
    // 类体:由成员函数和成员变量组成
};
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
号不能省略。
类体中内容称为类的成员; 类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方式:

1. 声明和定义全部放在类体中,需注意:成员函数如果 在类中定义 ,编译器可能会将其当成 联函数 处理。
2. 类声明放在 .h 文件中,成员函数定义放在 .cpp 文件中。
注: 成员函数名前需要加类名 ::
在如下代码中
class Date
{
public:
	void Init(int year)
	{
		// 这里的year到底是成员变量,还是函数形参?
		year = year;
	}
private:
	int year;
};
因此,对于 成员变量命名的建议如下
class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
private:
	int _year;
};

2.类的访问限定符

C++ 实现封装的方式: 用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。
访问限定符说明:
1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 ( 此处 protected private 是类似的 )
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C)
注:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

5.类的作用域

类定义了一个新的作用域 ,类的所有成员都在类的作用域中 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
	void PrintPersonInfo();//函数声明
private:
	char _name[20];
	char _gender[3];
	int _age;
};

//这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
    cout << _name << " "<< _gender << " " << _age << endl;
}

6.类的实例化

用类类型创建对象的过程,称为类的实例化

1. 类是对对象进行描述的 ,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
2. 一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
Person 类是没有空间的,只有 Person 类实例化出的对象才有具体的年龄。
3. 做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

7.类的对象

类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?那么我们如何计算一个类的大小呢?

对于如下代码

//类中既有成员变量,又有成员函数
class A1 
{
public:
	void f1() {}
private:
	int _a;
};

//类中仅有成员函数
class A2 {
public:
	void f2() {}
};

//类中什么都没有---空类
class A3
{};

分别计算大小,有

结论:一个类的大小,实际就是该类中 成员变量 之和,当然要注意内存对齐
注:空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
因此知道类的对象模型如下:

8.this指针

解释this指针之前,我们借用如下的日期类

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;  
	int _month; 
	int _day;   
};

int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}
Date 类中有 Init Print 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调用 Init
数时,该函数是如何知道应该设置 d1 对象,而不是设置 d2 对象呢?
C++ 中通过引入 this 指针解决该问题,即: C++ 编译器给每个 非静态的成员函数 增加了一个隐藏 的指针参数,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 成员变量 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成
this指针有如下特性
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。
2. 只能在 成员函数 的内部使用
3. this 指针本质上是 成员函数 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以 对象中不存储 this 指针
4. this 指针是 成员函数 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传 递,不需要用户传递

 因此,有

对于this指针还有如下一些问题:

1.this指针存在哪里?

答:因为this指针实际上是成员函数的一个隐藏参数,它在函数调用时被自动传递。在对象创建时,系统会为其分配一块内存空间,这个空间中包含了对象的成员变量和成员函数。当成员函数被调用时,this 指针会指向对象的内存空间,从而实现访问对象的成员变量和成员函数。因此this指针存放在对象里面。

2.this指针可以为空吗?

对于这个问题,这里给出两段代码

代码一:

class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

结果

代码二:

class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

结果

 对于结果我们能发现

对于代码一:p调用Print,不会发生解引用,因为Print的地址不在对象中。p会作为实参传递给this指针。this指针是空的,但是函数内访问_a,本质是this->_a,因此不能通过编译。

对于代码二:p调用Print,不会发生解引用,因为Print的地址不在对象中。p会作为实参传递给this指针。this指针是空的,但是函数内没有对this指针解引用,因此可以通过编译。

9.类的默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

1.构造函数

在C语言中,我们可能常常会忘了初始化数据或者销毁数据。而在C++中,对于一些特定场景来说,销毁数据时会略显繁琐,因此C++在类中引入了构造函数。

①定义

构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次

注:构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

②特征

它有如下的一些特征:

1. 函数名与类名相同。
2. 无返回值。
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;
};

在使用时, 也要注意使用方法

void TestDate()
{
	Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); //调用带参的构造函数

	//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	//以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
	Date d3();
}

对于d3编译器会给出警示 

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。

这里使用如下代码测试

class Date
{
public:
	/*	 
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date(int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}

	Date()
	{}
	*/

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

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

int main()
{
	Date d1;
	return 0;
}

1.将Date类中构造函数屏蔽

代码可以通过编译,因为编译器生成了一个无参的默认构造函数
2.将Date类中构造函数放开

代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用

6.对于这个默认构造函数,他对于内置类型成员不做处理(C++11中规定可以使用缺省值进行初始化)如下

class Stack
{
private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity = 0;
};

而对于自定义类型成员,它会去调用他的默认构造函数。

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

③结论

因此,可以有以下结论

1. 正常情况下,构造函数都需要我们自己去完成

2. 在以下情况下,可以不用书写构造函数

a. 内置的成员函数都已经有缺省值,且缺省值符合要求

b. 成员都为自定义类型的构造,且这些自定义类型都有自己定义的默认构造

④初始化列表

定义:

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

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

注:

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

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

a. 引用成员变量
b. const 成员变量
c. 自定义类型成员 ( 且该类没有默认构造函数时 )

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		,_ref(ref)
		,_n(10)
	{}
private:
	A _aobj;      // 没有默认构造函数
	int& _ref;    // 引用
	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 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

int main() 
{
	A aa(1);
	aa.Print();

	return 0;
}

 有

在这里,虽然在初始化列表中,分别初始化的是a1,a2,但是在声明中却是a2,a1,因此初始化列表中,先用a1初始化a2,而此时a1是随机值,因此便得到上图结果。所以,在初始化列表中的初始化顺序最好和类中的声明顺序相同,以防出现bug。 

⑤explicit关键字

用explicit修饰构造函数,将会禁止构造函数的隐式转换。

在介绍explicit之前,我们先了解下与类相关的隐式类型转换

对于下面这个类

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

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a;
};

 和int与double之间的隐式类型转换相似,用int初始化aa2时也发生了隐式类型转换

而在构造函数前加上explicit后,编译器将不再允许类型转换

explicit A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}

 

2.析构函数

在知道了构造函数初始化一个对象后,那么怎样结束一个对象呢?

①定义

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

②特性

它也是一个特殊的成员函数,具有如下一些特性:

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注:
a.析构函数不能重载
b.析构函数不对内置数据类型作出处理
c.如果自定义类型中本身就有的话,会调用他的析构函数

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

③适用场景

析构函数的适用场景:

1.有动态内存申请资源时,就需要显示写析构函数释放资源
如对于下面这个栈类来说,我们就需要手动写出析构函数

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack()" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = capacity;
		_top = 0;
	}

	~Stack() 
	{
		cout << "~Stack()" << endl; 
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity;
};

2.没有动态内存申请的资源,不需要写析构

如下面这个日期类

class Date 
{
private:
		int _year;
		int _month;
		int _day;
};

3.需要释放资源的成员都是自定义类型,不需要写析构
如用栈实现一个队列,那么这个队列也不需要写析构

class MyQueue
{
private:
	Stack _pushst; 
	Stack _popst;
};

3.拷贝复制函数

在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?

①定义

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

②特征

 拷贝构造函数也是一个特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式

2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错
因为会引发无穷递归调用。
对于这个无穷递归,首先我们要知道C++中规定:
a.内置类型直接拷贝
b.自定义类型必须调用拷贝构造完成拷贝

(在这里还是采用日期类举例)

class Date 
{
public:
    
    //第一种
	Date(Date d) 
	{
		cout << "Date(Date d)" << endl; 
		_year = d._year;
		_month = d._month; 
		_day = d._day;
	}

    //第二种
    Date(Date& d) 
	{
		cout << "Date( Date& d)" << endl; 
		_year = d._year;
		_month = d._month; 
		_day = d._day;
	}

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

 如果使用第一种拷贝构造函数的话,会有如下的过程

传值得话,需要先把这个对象构造出来,那就要调用拷贝构造,那又要构造新的对象,这最终导致陷入无限的递归中,

而如果采用第二种的话就没有这样的忧虑,因为传入的是自定义类型对象的引用而非对象,这样就可以直接操作传入的对象,而不用再次构建新的对象。

3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按

字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
对于没有动态内存资源开辟,或者或者已经有自定义类型内部有开辟动态内存函数的类都不需要自己写一份深拷贝的拷贝构造函数。
而对于有动态内存开辟需要的类,则不能进行浅拷贝,如拷贝一个栈类的对象,有

因为逐字节拷贝,所以st2._a与st1._a所指向的空间为同一份,而且在st1与st2的生命周期结束时,它们指向的这个空间会被释放两次,这样的结果显然不是我们想要的。

而下面的深拷贝才是我们想要的

4.赋值运算符重载

①运算符重载

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

函数原型: 返回值类型  operator 操作符 ( 参数列表 )
注:
1. 不能通过连接其他符号来创建新的操作符:比如 operator@
2. 重载操作符必须有一个类类型参数
3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
4. 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的this
5.   .*   ::   sizeof   ?:   .    注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。

这里还是使用日期类来进行举例

operator可以有全局和封装在类内部两种

// 全局的operator==
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};

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

这里我们发现运算符重载成全局的,而成员变量是私有的,这种情况不好处理,因此我们一般选择将其封装到内部。 

// 封装的operator==
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// bool operator==(Date* this, const Date& d2)
	// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date & d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

②赋值运算符重载

格式:
参数类型 const 类名& ,传递引用可以提高传参效率
返回值类型 :类名 & ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回 *this :要复合连续赋值的含义
举例:
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

注:

a. 返回*this返回的是this指向的内容,为这块空间取别名并返回没有问题

b. 赋值运算符只能重载成类的成员函数不能重载成全局函数,因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
c. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。在这里 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。这里与前面的拷贝复制函数差不多,当涉及到动态内存分配时,需要手动实现深拷贝,而不能使用浅拷贝(值拷贝)。

③前置++与后置++的重载

因为前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载, C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
前置++:返回+1之后的结果
Date& operator++()
{
	_day += 1;
	return *this;
}
后置++
Date operator++(int)
{
	Date temp(*this);
	_day += 1;
	return temp;
}
注:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1

④取地址const取地址操作符重载

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
Date* operator&()
{
	return this;
}
const Date* operator&()const
{
	return this;
}

10.const成员

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

举个例子

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

	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}

	void Print() const
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

void Test()
{
	Date d1(2022, 1, 13);
	d1.Print();
	const Date d2(2022, 1, 13);
	d2.Print();
}

运行如下

在这里const写在Print函数之后,实际是是将Date* const this转换为const Date* const this,即先前this指针不能更换指向对象,现在this指针指向的内容也不能更改(即无法通过this指针来修改对象的状态)。即

 注意,在C++中被const修饰过的变量会变成一个常量,具有替换的作用,即当再次遇见它时会直接将其进行类似于#define的操作,让我们来看下面的这段代码

#include <iostream>

using namespace std;

int main()
{
	const int a = 10;
	int* p = (int*)(&a);
	*p = 20;
	cout << "a = " << a << ", *p = " << *p << endl;
	return 0;
}

在这里指针指向的空间可以被指针直接修改,但是当打印a时,由于编译器在编译阶段时,在程序中看到对常量的内存读取时,会直接使用常量中的内容替换该常量,因此可以得到如下结果

 11.static成员

①概念

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

下面用两个面试题来举例

实现一个类,计算程序中创建出了多少个类对象。 

class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }
	static int GetACount() { return _scount; }
private:
	static int _scount;
};

int A::_scount = 0;

void TestA()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	cout << A::GetACount() << endl;
	A a3(a1);
	cout << A::GetACount() << endl;
}

 运行有

设计一个类,在类外面只能在栈上创建对象
设计一个类,在类外面只能在堆上创建对象 

class A
{
public:
	static A GetStackObj()
	{
		A aa;
		return aa;
	}

	static A* GetHeapObj()
	{
		return new A;
	}
private:
	A()
	{}

private:
	int _a1 = 1;
	int _a2 = 2;
};

int main()
{
	//static A aa1;   //  静态区
	//A aa2;          //  栈 
	//A* ptr = new A; //  堆
	A::GetStackObj();
	A::GetHeapObj();

	return 0;
}

②特性

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

注:在有const修饰时,静态数据成员可以在类内初始化,如

class MyClass {  
public:  
    static const int myStaticConst = 10;  
};

12.友元

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

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

①友元函数

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

这里就用日期类(C++应用实例—日期类的实现)里的流操作符举例

问题:

尝试重载 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 = 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;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

 运行有

注:

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

②友元类

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

举例

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;
};

注:

1. 友元关系是单向的,不具有交换性。
比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
2. 友元关系不能传递
如果 C B 的友元, B A 的友元,则不能说明 C A 的友元。
3. 友元关系不能继承,在继承位置再给大家详细介绍。

13.内部类 

①概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访
问外部类中的所有成员。但是外部类不是内部类的友元。  
举例
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;
			cout << a.h << endl;
		}
	};
};

int A::k = 1;

int main()
{
	A::B b;
	b.foo(A());

	return 0;
}

②特性

1. 内部类可以定义在外部类的 public protected private 都是可以的。
2. 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
3. sizeof( 外部类 )= 外部类,和内部类没有任何关系。

14.匿名对象

①概念

在 C++ 中,匿名对象是指没有被命名的对象,即没有通过变量名或引用名来指向它的对象。通常情况下,匿名对象是通过在对象类型后面直接添加一对括号来创建的

举例

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution 
{
public:
	int Sum_Solution(int n) 
	{
		//...
		return n;
	}
};
int main()
{
	A aa1;

	// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();
	

	// 但是我们可以定义匿名对象,匿名对象的特点不用取名字,
	// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A(1);
	A aa2(2);
	// 匿名对象在这样场景下就很好用
	Solution().Sum_Solution(10);
	return 0;
}

 对于A(1)逐语句调试发现有

注:若对匿名对象取引用,则会使匿名对象的生命周期延长至该函数范围内。 

②特性

  1. 匿名对象没有名字,只能通过创建它的表达式来访问它。

  2. 匿名对象通常只能用于一次计算或者函数调用中,因为它们没有名字,无法被其他代码所引用,也无法进行修改。

  3. 匿名对象的生命周期通常比较短暂,一般在创建它的表达式计算完毕后就会被立即销毁。

  4. 匿名对象可以作为函数的参数传递,但是传递后就无法再次使用了。

  5. 匿名对象也可以作为函数的返回值,但是需要注意的是,如果返回的是对象的指针或者引用,那么需要确保匿名对象的生命周期足够长,以免出现未定义行为。

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值