首页 > 编程语言 >C++类与对象(三)

C++类与对象(三)

时间:2024-09-20 11:19:21浏览次数:3  
标签:初始化 对象 成员 C++ int 静态 class 构造函数

目录

1.再谈构造函数

1.1 构造函数体赋值

1.2 初始化列表

1.3 explicit关键字

2.STATIC成员

2.1 概念

2.2 特性

3.C++中成员初始化的新玩法

4.友元

4.1 友元函数

4.2 友元类

5.内部类

6.再次理解封装

7.再次理解面向对象


本次内容大纲:

1.再谈构造函数

1.1 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
 {
 public:
    //构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
 
private:
    int _year;
    int _month;
    int _day;
 };

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

class Date
 {
 public:
    //构造函数
    Date(int year, int month, int day)
    {
        _year = year;  //第一次赋值
        _year = 2024;  //第二次赋值
        //.....多次赋值

        _month = month;
        _day = day;
    }
 
private:
    int _year;
    int _month;
    int _day;
 };

在函数体中,我们可以多次给成员对象赋值,而不是初始化

1.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 d1(2024, 9, 15);
	return 0;
}

注意:

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

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

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

引用成员变量和const成员变量都要一个相同的特点,在定义时必须初始化,所以它们只能使用初始化列表初始化

引用创建时必须初始化

int& a; //error

int b = 10;
int& a = b;  //创建就初始化

const变量创建时必须初始化

const int a; //error  创建时未初始化

const int a = 10; //创建时初始化

自定义成员:

若类中没有默认构造函数,我们实例化该类对象时应该传参对其初始化,即:初始化没有默认构造函数的类对象时,必须使用初始化列表来对其进行初始化

在这里说明一下,默认构造函数是指不用传参就可以调用的构造函数:

1.我们不写构造函数,编译器自己生成的

2.无参的构造函数

3.全缺省的构造函数

typedef int DateType;
class Stack
{
public:
	//构造函数
	Stack(int capacity)
	{
		_a = (DataType*)malloc(sizeof(DataType) * capacity);
		if(nullptr == _a)
		{
			perror("Stack::malloc");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	//析构函数
	~Stack()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_top = _capacity = 0;
		}
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class A
{
public:
	A(int& ref, int a)
		:_ref(ref)
		,_a(a)
		,_st(10)
	{}
private:
	int& _ref;    //引用
	const int _a = 2; //const

	Stack _st;    //没有默认构造函数
};

int main()
{
	int p = 10;
	A a(p, 20);
	return 0;
}

在定义时就得初始化得变量类型,就必须放在初始化列表进行初始化

还有一个要注意得是,可以看到在构造函数的参数中其中ref引用的意义是防止野引用

如果这里是int ref的话,将一个局部变量给_ref初始化,就会导致_ref引用到一个被释放的空间

在类中声明时给值const int _a = 2;,这个意思是给初始化列表缺省值,如果我们没有在初始化列表对_a进操作,_a就会被赋值为2

3. 尽量使用初始化列表初始化

当你实例化一个对象时,初始化列表就是对象成员定义的地方,无论你是否使用初始化列表,每个对象成员都会走一遍初始化列表(成员变量需要定义出来)

对于内置类型,使用初始化列表和在构造函数体内初始化实际上没有什么区别,其差别就类似以下代码:

//使用初始化列表初始化
int a = 10;

//使用函数体初始化(没有使用初始化列表)
int a;
a = 10;

对于自定义类型,使用初始化列表还可以提高代码效率

class Time
{
public:
	Time(int hour = 1)
	{
		_hour = hour;
	}
private:
	int _hour;
};

class test
{
public:
	test()
		:_t(12)  //调用拷贝构造函数
	{}
private:
	Time _t;
};

在类test中,我们调用了一次类Time中的构造函数

在不使用初始化列表的情况下:

class Time
{
public:
	Time(int hour = 1)
	{
		_hour = hour;
	}
private:
	int _hour;
};

class test
{
public:
	test(int hour)
	{
       //初始化列表调用了一次构造函数(不使用初始化列表也会走这个流程)
        Time tmp(hour); //调用一次构造函数
        _t = tmp;    //调用赋值运算符重载函数
    }
private:
	Time _t;
};

与上面那段代码对比,这里多调用一次构造函数和赋值运算符重载函数

初始化野不能解决我们百分之百的初始化问题

例如:

情况一:

typedef int DataType;

class Stack
{
public:
	//构造函数
	Stack(int capacity)
		:_a((DataType*)malloc(sizeof(DataType)* capacity))
		, _top(0)
		,_capacity(capacity)
	{
		//我们需要_a做检查,初始化列表完成不了
		if(nullptr == _a)
		{
			perror("Stack::malloc");
			exit(-1);
		}

		//我们还需要_a数组初始化一下,初始化初始化列表也完成不了
		memset(_a, 0, sizeof(int) * _capacity);
	}

	//析构函数
	~Stack()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_top = _capacity = 0;
		}
	}
private:
	DataType* _a;
	int _top;
	int _capacity;
};

情况二:

使用构造函数申请一个二维数组

class AA
{
public:
	AA(int row, int col)
		:_row(row)
		,_col(col)
	{
		_a = (int**)malloc(sizeof(int*) * row);
		for (int i = 0; i < col; ++i)
		{
			_a[i] = (int*)malloc(sizeof(int) * col);
		}
	}
private:
	int** _a;
	int _row;
	int _col;
};

初始化列表并不能帮我们完成初始化所有的事情

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

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

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};

//A.输出1  1
//B.程序崩溃
//C.编译不通过
//D.输出1  随机值

测试运行:

答案:D

1.3 explicit关键字

构造函数不仅可以初始化和赋值,对于单个参数的拷贝构造函数,还支持隐式类型转换

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
private:
	int _a;
};

int main()
{
	A a(1);
	A b = 2;  //隐式类型转换,从内置类型到自定义类型
	return 0;
}

对于单个参数的拷贝构造函数,编译器是允许这样调用构造函数的,实际上这里还调用了一次拷贝构造函数

在语法上这里一段代码A b = 2;等价于下面两句代码:

A tmp(2); //调用构造函数
A b = tmp; //调用拷贝构造函数

先构造,在拷贝构造

早期的编译器就是这么处理的,当编译器遇到A b = 2;时它会先调用构造函数创建一个临时对象,然后讲这个临时对象拷贝构造给b。但是现在编译器已经做了优化,遇到A b = 2;时会按照A b(2);来进行处理,只会调用一次构造函数,这就是隐式类型转换

在我们仪以前的学习中,其实我们也遇到过隐式类型转换,例如:

int a = 10;
double b = a;

在这个过程中,为了保护a的值不被破环,会产生一个临时变量来存放a的值,然后将这个临时变量赋值给b

从汇编可以看到确实是这样操作的

但是,A b = 2;这种形式的可读性不是很好,所有我们可以使用explicit关键字来修饰构造函数,这样就可以禁止单参数构造函数的隐式类型转换

可以看到加入explicit之后就报错了

2.STATIC成员

2.1 概念

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

2.2 特性

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

例:

可以看到类中的静态变量不计入类大小,因为它存在于静态区,但是它属于类,属于这个类所有对象的成员

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

class A
{
private:
	static int _a;
};

int A::_a = 10;

单独一个类对象成员定义的地方是初始化列表,但是_a是静态成员变量,属于所有A类对象的成员,不是属于某一个对象的,所有它不能使用初始化列表,也不能给初始值,所以它是一个特例,不受访问限定符的影响,在全局域初始化

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

例:

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

int A::_a = 10;

int main()
{
	A a(5);
	a.Print();
	return 0;
}

静态成员函数里面不能使用非静态成员

tips:

含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量

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

(1)当静态成员变量公有时,有以下几种访问形式

class A
{
public:
	int _p;
	static int _a;
};

int A::_a = 10;

int main()
{
	A a;
    cout << a._a << endl;   //使用类对象突破类域对静态成员进行访问
	cout << A()._a << endl; //使用匿名结构体突破类域对静态成员进行访问
	cout << A::_a << endl;  //使用类名突破类域对静态成员进行访问
}

(2)当静态成员变量是私有时,有以下几种访问形式

class A
{
public:
	int _p;
    //没有this指针,指定类域和访问限定符就可以访问
	static int GetACount()
	{
		return _a;
	}
private:
	static int _a;
};

int A::_a = 10;

int main()
{
	A a;
	cout << a.GetACount() << endl;   //通过对象调用静态成员函数来进行访问
	cout << A().GetACount() << endl; //通过匿名对象调用静态成员函数来进行访问
	cout << A::GetACount() << endl;  //通过类名调用静态成员函数进行访问
	return 0;
}

这里说明一下:

使用非静态成员函数只能 得到_a的值,来使用它的值,并不能访问_a

class A
{
public:
	int _p;
	int GetACount()
	{
		return _a;
	}
private:
	static int _a;
};

int A::_a = 10;

int main()
{
	A a;
	cout << a.GetACount() << endl;   //通过对象调用静态成员函数来得到_a的值
	
	return 0;
}

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

当静态成员是private时,尽管突破了类域也不能对其进行访问

【问题】

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

不可以,因为非静态成员函数第一个形参是this指针,而静态成员函数中没有this指针,所以静态函数成员不能调用非静态函数

2. 非静态成员函数可以调用类的静态成员函数吗?

可以,因为在类域中静态成员函数不受类域和访问限定符的限制

使用static情景:

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

class A
{
public:
	static A GetStackObj()
	{
		A N1;
		return N1;
	}
private:
	A()
	{}
private:
    int _a;
    int _b;
};

int main()
{
    A::GetStackObj();
    return 0;
}

3.C++中成员初始化的新玩法

C++11支持非静态成员变量在声明时进行初始化,但是要注意这里不是初始化,而是给初始化列表一个缺省值

class A
{
public:
	A()
	{}

private:
    //非静态成员声明时可以给缺省值
	int* _a = (int*)malloc(sizeof(int) * 10); 
	int _b = 10;

	static int _c;  //非静态对象不能直接给缺省值
};

初始化列表是你初始化定义的地方,如果你没有给值,那它就会使用你给的缺省值,如果你给了值,那么它就不会使用你的缺省值

4.友元

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

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

4.1 友元函数

问题:现在尝试去重载operator<<,然后发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator>>重载成成员函数但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

我们知道在C++有一件很神奇的事,就是cout和cin可以自动识别输入和输出对象的类型,我们使用它们时不用像C语言一样增加输入输出的格式,这给我们提供了便利,难道C++真有这么神奇吗?其实不是,内置类型的变量可以直接使用cout和cin,是因为它们在库中已经实现了内置类型于运算符的重载

可以查看到,cout在ostream这个类中,而cin在istream这个类中

我们来实现一下自定义运算符的重载,让自定义类型也可以使用<<和>>

这里友元函数就起到作用了

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>> (istream& _cin, Date& d);
public:
	Date(int year = 1, 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 d1;
	cin >> d1;
	cout << d1;

    return 0;
}

这里上一节更详细大家有兴趣的可以去看看这里是链接  点击这里

注:

cout是ostream类中的一个全局对象,cin是istream类中的一个全局对象,<<和>>重载具有返回值,是为了连续的实现<<和>>的连续运算,它们的生命周期都是整个工程

说明:

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

友元函数不能用const修饰

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

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

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

4.2 友元类

声明友元类之后,Date中你所有成员函数都是Time中的友元函数,即:Date中的所有成员函数都可以访问Time中的私有成员

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

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

	//使用Time类
	void SetTimeOFdate(int hour = 1, int minute = 1, int second = 1)
	{
		T._hour = hour;
		T._minute = minute;
		T._second = second;
	}

private:
	int _year;
	int _month;
	int _day;

	Time T;
};

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

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

5.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

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

特性:

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

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

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

class A 
{
private:
	static int _a;
	int _b;
	//B是A的内部类,B类是A类的天生友元
	class B
	{
	private:
		void foo(A& a)
		{
			cout << a._b << endl;
			cout << _a << endl;
		}
	};
};

计算大小为:4

这里A的大小为4,内部类不算做整体的大小

6.再次理解封装

C++是一门面向对象的语言,面向对象编程语言具有三个特征:封装,继承,多态

C++通过类,将对象的属性和行为封装在一起,就像去旅游一样,去参观一个景点,需要先预约买票,然后排队进入,进去之后只供观赏。若兵马俑可以让人随意的触摸,你一下我一下这些文物能保留多久呢?

所以我们去参观兵马俑时,他只允许我们观赏和拍照,并不允许我们做其他操作,这就是属于封装,通过将景点封装起来提供一个行为供我们使用。防止物品不让其他人破坏

博物馆管理系统

售票系统:负责将票卖给游客

工作人员: 检票,打扫,服务,安保,卫生等

导游:带领客户到指定位置参观博物馆,并且给客户讲解物品的缘由,历史等

通过对比理解,其实C++也是一样,将对象的属性和行为封装在一个类中,通过访问限定符,对类中的内容来进行管理,将用户需要的,我们能提供的设置为:public,为用户提供一个接口,至于其中实现的原理,用户就不需要知道,知道反而会增加使用或者维护的难度

7.再次理解面向对象

其实面向对象就是模拟抽象现实世界

标签:初始化,对象,成员,C++,int,静态,class,构造函数
From: https://blog.csdn.net/m0_73634434/article/details/142188162

相关文章

  • C++扫盲--直接构造(Direct Initialization)
      在C++中,直接构造(DirectInitialization)是由一种对象构造的方式,它直接调用类的构造函数来初始化对象。这种方式通常用于创建对象时立即提供必要的参数。直接构造的语法如下:ClassNameobjectName(arguments);其中,ClassName是类的名称,objectName是要创建的对象的名称,argument......
  • C++20 模块化(Modules)
    C++20引入的模块化(Modules)是一个重大改进,旨在取代传统的头文件机制,提高编译速度、代码可维护性以及项目的可扩展性。模块化为C++提供了一种更现代化的代码组织方式,避免了头文件中常见的宏污染、重复编译和复杂的依赖管理问题。概念与背景在C++20之前,C++项目是通过头文......
  • VS(visual studio) C++ 封装dll,以及其隐式调用与显式调用(静态\动态)
    DLL介绍DLL(动态链接库,DynamicLinkLibrary)是一种可执行文件,它包含可以在其他程序中调用的函数和数据。他是Windows操作系统中的一个重要概念,用于代码共享和模块化。特点代码共享:多个程序可以同时使用同一个DLL文件,而不需要将其代码编译到每个程序中。这样可以节省磁盘空间和......
  • C++使用Win32GDI DC进行屏幕截图
    代码#include<windows.h>#include<Psapi.h>#include<algorithm>#include<cstdio>#include<cstdlib>#include<cstring>#include<iostream>#include<memory>#include<string>#include<thread>#......
  • 【C++】C++ STL探索:Priority Queue与仿函数的深入解析
    C++语法相关知识点可以通过点击以下链接进行学习一起加油!命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇类和对象-下篇日期类C/C++内存管理模板初阶String使用String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与Queue这篇文......
  • C++ std::find函数 容器元素查找
    简介std::find函数是C++标准库内非常实用的一个函数,主要用于在给定范围内查找某个元素,如果找到该元素,则返回指向该元素的迭代器;如果没有找到,则返回指向范围末尾的迭代器(即 end() )。find函数原型std::find在头文件algorithm中template<classInputIt,classT>Inp......
  • C++ | 引用详解
    文章目录C++引用详解一、什么是引用二、引用的语法三、引用的特点1.必须初始化2.与原始对象具有相同的内存地址3.没有独立的存储空间4.传递参数高效四、引用的用途1.函数参数传递2.返回值3.用于实现运算符重载五、引用与指针的区别1.语法2.初始化3.空值4.操......
  • C++标准的一些特性记录:C++11的thread_local
    文章目录thread_localthread_local在多线程的编程环境里,一般来说,所有的线程都是共享同一个内存空间,也就是说如果定义一个变量,这个变量是被所有线程共享的,所以多个变量在访问同一个变量时,是需要加锁机制的,否则就会出现问题。在C++11中,引入了一个关键字thread_local......
  • C++代码编译过程
    代码转换成可执行文件的全过程看了一些网上的文章,觉的还是自己写写看看才能理解通透,也为找简历做准备。代码转换成可执行文件分为4个步骤,预处理、编译、汇编、链接。预处理这个过程是执行代码中的一些预处理指令,多说无益,直接上代码#include<iostream>#defineBesttrue;#i......
  • 蓝桥杯十五届软件赛C++B组题解
    最近蓝桥杯官网已经把十五届题目上架了,我会尽快的将题解发出来,没有发的过段时间再补。​​​​​​​数字接龙一个很鹅心的搜索题,一不注意就会写错,比赛的时候写不来,题目上架后也WA了两个样例才过。题目大意:也就是说从(1,1)开始 ,下一步路的数据总是要比当前数据大1,超过k就......