C++类和对象(三)

发布于:2025-07-27 ⋅ 阅读:(14) ⋅ 点赞:(0)

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!

目录

1.构造函数

1.1.构造函数体赋值

1.2 初始化列表

1.3 explicit关键字

2.Static成员

2.1.概念

2.2.静态成员函数

2.3.总结

3. 友元

3.1.友元函数

3.2.友元类

4. 内部类

5.匿名对象


1.构造函数

1.1.构造函数体赋值

        如下,在对象实例化的过程中,成员变量的创建时机是在进入构造函数之前;构造函数体中的代码是在成员变量已经创建后执行的,所以属于赋初值,不是初始化。
// 构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

        上述代码中的成员变量都是整型在创建的时候可以不用赋予初始值。 

1.2 初始化列表

       C++规定通过初始化列表(而非构造函数体),才能真正实现成员变量的初始化。初始化列表在成员变量创建时直接赋值,且仅执行一次。所以初始化列表就是成员变量定义的地方。

        其构成为以一个冒号开始,以逗号分隔的数据成员列表,每成员变量后面跟一个放在括号中的初始值或表达式。如下:

//初始化列表
	Date(int year, int month, int day)
		:_year(year)
        , _month(month)
        , _day(day)
	{  }

需要注意的是:

  1. 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量 ,const成员变量,自定义类型成员(且该类没有默认构造函数时)。
  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。 也就说,不管写不写初始化列表,在对象创建的时候,都会经过初始化列表。
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
  5. 函数体和初始化列表都是构造函数的一部分,初始化列表的存在可以分担一些初始化的操作,但是在一些情况下不能独立完成初始化操作。

注意1 说明:

        初始化的核心含义是:为变量分配内存空间,并赋予其初始值的过程。这个过程具有 一次性的特性,一旦变量完成初始化,其内存空间就已确定,后续对它的操作只能是 “赋值”(修改已有内存中的值),而不是 “再次初始化”(重新分配内存)。

        总之:初始化就是:内存分配 + 赋初值

class Stack 
{
public:
	Stack(int capacity = 10)
		:_a((int*)malloc(sizeof(int)*capacity))
		, _size(0)
		, _capacity(capacity)
	{}


private:
	int* _a;
	size_t _size;
	size_t _capacity;
	

};
class MyQueue
{
public:
	MyQueue()
	{}
	MyQueue(int capacity)
		:_pushst(capacity)
		,_popst(capacity)

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


int main()
{

	MyQueue mq1;
	MyQueue mq2(100);

	return 0;
}

        上述程序的结果说明,在具有默认构造函数的自定义类型,可以不用在初始化列表中写出。

注意2 说明:

        引用成员变量 ,const成员变量其必须在定义的时候初始化;因此必须采用初始化列表的方式初始化;对于有默认构造函数的自定义成员变量,初始化可以不传参数,但是没有的情况下,必须传递参数是自定义类型成员初始化。

class A
{
public:
	A(int a = 4)
		:_a(a)
	{}
private:

	int _a;
};

class B 
{
public:
	B(int& ref, int a)
		:_ref(ref)
		, _a(a)
		, _x(1)
	{}

private:
	A _aobj;
	int& _ref;
	const int _a;
	int _x;
};


        上述代码中含有内置类型和自定义类型,int _x=1;其中的1是缺省值,但是其可以在初始化的时候使用,虽然没有调用A类的初始化,但是程序会自动调用其初始化列表。测试程序:

int main()
{
	int n = 3;
	B bb1(n, 2);
	return 0;
}

运行结果

注意4 说明

class Stack
{
public:
	Stack(int capacity = 10)
		: _size(0)
		, _capacity(capacity)
		,_a((int*)malloc(sizeof(int)* _capacity))
	{
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
	}
private:
	int* _a;
	size_t _size;
	size_t _capacity;

};

int main()
{
	Stack st1;
	return 0;
}

        上述代码将_a的定义放到最后,其采用的初始化内存空间的值,是_capacity;在初始化列表中,按照是声明的顺序来初始化的,因此,在初始化_a的时候,_capacity仍然是一个随机值,开辟的空间大小也是一个随机值。

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

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	Test aa(1);
	aa.Print();
}

上述的输出:

        证明初始化是有顺序的,先初始化的a1再初始化的a2。

1.3 explicit关键字

         构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数
总结:在创建新的对象的时候,仅仅传递 一个参数就可以完成对象的初始化。

引入:

class D
{
public:
	D(int a)
		:_a(a)
	
	{ 
		cout << "Init" << endl;
	
	}
	 D(const D& d) 
	{
		 cout << "Copy" << endl;
		 _a = d._a;
	}

private:
	int _a;
};
int main()
{
	D dd1(5);
	D dd2 = 7;//隐式类型转换 7以D类构造函数创建新的临时对象,然后根据临时对象拷贝构造生成dd2
	//但是编译器会优化为 dd2以7直接优化的。

	//反例
	//D& dd3 = 9;
	const D& dd3 = 9;   //证明会产生临时变量 会调用一次 构造函数 
	D dd4(dd3);         

	//内置类型转换
	int a = 10;
	double d = a;//隐式类型转换 a转换为中间变量 double的类型然后再赋值给d

}

        在上述代码中,为了提高效率,连续的调用构造,编译器一般都会优化,将连续的构造整合为一个。

        根据反汇编,可以看出,仅仅调用了一次构造,但是其特性不能忘记,中间变量具有常性,为了方式隐式类型转换,

可以在构造函数中加上explicit ,不让其构造支持隐式转换。

class D
{
public:
	explicit D(int a)
		:_a(a)
	
	{ 
		cout << "Init" << endl;
	
	}
	 D(const D& d) 
	{
		 cout << "Copy" << endl;
		 _a = d._a;
	}

private:
	int _a;
};

2.Static成员

引入:计算程序中有多少的对象。

int _count = 0;
class B
{
public:
	B()
	{
		_count++;
	}
	B(const B& b) 
	{
		_count++;
	}
	~B()
	{
		_count--;
	}
private:

};

B& Func(B bb) // 进入调用拷贝构造一次创建新的对象  // 4
{
	B bb4;   // 5
	cout << __LINE__ << ": " << _count << endl; // 5
	return bb4; // 销毁 bb bb4    3
}
B bb1;   // 1

int main()
{
	cout << __LINE__ << ": "<<_count << endl;

	B bb2;  // 2
	static B bb3; // 3
	cout << __LINE__ << ": " << _count << endl;
	B bb5 = Func(bb3);    //4
	cout << __LINE__ << ": " << _count << endl;
	return 0;
}

        由于全局变量的修改在程序的任何地方都可以修改,因此在C++中将全局变量封装在类中,只供一个类的使用,给静态变量增加一个约束。

2.1.概念

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

class B
{
public:
	B(){_count++;}
	B(const B& b){_count++;}
	~B(){_count--;}
private:
	// 普通成员变量
	int _b1 = 1;
	int _b2 = 2;
	// 静态成员变量
	static int _count;
};
// 静态成员变量支持类外定义
int B::_count = 0;

        静态成员变量属于类本身,并非类的某个对象,所以它需要在类外进行单独定义,从而为其分配内存空间。

定义位置不受访问限定符的限制

       无论静态成员变量在类中被声明为privateprotected还是public,其定义都能在全局作用域内完成。这是因为:

  •  声明时的访问限定符,是针对访问行为的限制,而非定义行为
  •  定义静态成员变量属于类的实现细节,只要在编译时能访问到类的完整定义,就可以进行定义。

2.2.静态成员函数

class B
{
public:
	B(){_count++;}
	B(const B& b){_count++;}
	~B(){_count--;}
	// 静态成员函数没有传递 this指针,指定类域或者修改访问限定符就可以访问
	static int GetCount()
	{
		return _count;
	}
private:
	// 普通成员变量
	int _b1 = 1;
	int _b2 = 2;
	// 静态成员变量
	static int _count;
};
// 静态成员变量支持类外定义
int B::_count = 0;

int main()
{
	B bb1;
	cout << B::GetCount() << endl;
	return 0;
}

        私有的成员变量访问的时候可以通过GetCount函数,但是一般的成员函数访问需要又对象,传递this指针,为了方便调用,static函数诞生了,这种函数是没有外部对象的条件下调用成员函数。静态变量和静态成员函数一般会成对存在。

引例:

// 设计一个类,在类外面只能在栈上创建对象
// 设计一个类,在类外面只能在堆上创建对象
class C
{
public:
	static C GetStackObj()
	{
		C cc;
		return cc;
	}
	//在成功分配内存后,new会返回一个指向所分配类型对象的指针。
	static C* GetHeapObj()
	{
		return new C;
	}
	// 上述两个函数是在类内创建的对象,想要在类外不用对下个直接调用,就加上static
private :
	int _a1 = 1;
	int _a2 = 2;

};

int main()
{
	C::GetHeapObj;
	C::GetStackObj;
	return 0;
}

2.3.总结

静态成员变量:

         静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区;所以在创建的类的时候静态成员就已经初始化了;因此不要将其和普通的成员变量混淆,静态成员变量输出类的本身,其他的成员变量是蓝图。所以其定义的时候要在类外定义,不参加对象的初始化。当然静态成员函数也是受访问限定符限制的。

静态成员函数:

        静态成员函数没有this指针,不能访问任何非静态成员,包括函数非静态成员的函数以及普通的成员函数。如下:随便创建的空函数,也是不可以访问或者复用的。,

3. 友元

3.1.友元函数

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

特点

  • 非成员函数:友元函数不属于类,定义时不需要加类作用域前缀(如 MyClass::)。
  • 访问权限:可以直接访问类的私有和保护成员,但必须通过对象实例之后才可以访问(如 obj.privateVar)。
  • 声明位置:友元声明可以放在类的任意位置(public、protected 或 private),效果相同。

举例:运算符号 <<  >> 的重载

class Date
{
public:
	Date(int year = 2025, int month = 7, int day = 26)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1 << cout;
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	return 0;
}

        上述代码中的<< 的第一个操作数是d1也就是this指针;第二个操作数cout,相反了。为了转过来,只能写到类的外部,但是还要访问函数内部私有的成员变量;所以有了 friend关键字。

class Date
{
public:
	Date(int year = 2025, int month = 7, int day = 26)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	friend	ostream& operator<<(ostream& out, Date& d);

private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out,Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}
int main()
{
	Date d1;
	cout << d1;
	return 0;
}

        上述代码中,对流输出的运算符重载函数写在类的外面。

friend	ostream& operator<<(ostream& out, Date& d);

        让重载函数突破访问限制;类外函数可以访问类内私有成员

友元函数不推荐多用,除去友元函数 Get Set函数,因为友元增加了耦合;让关联度更加紧密,修改类中成员不好修改。

流输入

istream& operator>>(istream& cin, Date& d)
{
	cin >> d._year;// 空格代表这一次输入命令的结束
	cin >> d._month;
	cin >> d._day;
	return cin;
}

3.2.友元类

        友元类是一种允许一个类访问另一个类私有(private)和保护(protected)成员的机制。通过友元类,被授权的类可以突破封装限制,直接操作另一个类的内部数据。        

        特性

  • 单向性:如果 A 声明 B 为友元类,B 可以访问 A 的私有成员,但 A 不能访问 B 的私有成员,除非 B 也显式声明 A 为友元。
  • 非继承性:友元关系不能被继承。即使 B 是 A 的友元,B 的派生类也不会自动成为 A 的友元。
  • 非传递性:如果 A 是 B 的友元,B 是 C 的友元,C 不会自动成为 A 的友元
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;
};

4. 内部类

        内部类(嵌套类) 是定义在另一个类内部的类。

特性

  1. 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
  2. 内部类就是外部类的友元类;
  3. 内部类可以在外部类的public、protected、private任何部分。
  4. 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  5. sizeof(外部类)=外部类,和内部类没有任何关系。 内部类独立存在:外部类和内部类的对象在内存中是分离的,彼此不包含对方。
例子:
 
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void Func(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.Func(A());
	return 0;
}



5.匿名对象

       匿名对象(Temporary Object) 是一种未被命名的临时对象,它在表达式中创建后立即使用,通常在当前语句结束后销毁。是一种的临时值传递方式,常用于简化代码或作为函数参数。

实例:匿名在同一行创建,在同一行销毁。

class C
{

public:
	C(int c = 1) 
		:_c(c)
	{
		cout << "Init" << endl;
	}
	//拷贝构造函数
	 C(C& cc)
	{
		_c = cc._c;
	}
	 void Func() const
	 {
		 cout << _c << endl;
	 }
	~C()
	{
		_c = 0;
		cout << "Destroy" << endl;
	}

private:

	int _c;

};
int main()
{
	// 普通对象
	C cc1(10);
	cc1.Func();
	//匿名对象
	C(20); 
	C(20).Func();
	//匿名对象具有常性 
	const C& rc = C(30);
	rc.Func();

	return 0;
}

总结

1.匿名对象可以调用一次成员函数,普通对象可以调用多次;

2.匿名对象在不被使用的赋值的情况下,用完即销毁;

3.在被赋值的情况下,匿名对象会和被赋值的对象的生命周期一样;

匿名对象的使用

void push_back(const string& s)
{
	cout << "push_back:" << s << endl;
}

int main()
{
    //传递命名对象(左值)
    string str("11111");
    push_back(str);
    //传递匿名对象(右值)
    push_back(string("222222"));
    //传递字面量(隐式类型转换)
    push_back("222222");

    return 0;

}
 push_back(str):传递命名对象(左值)
  • 过程str 是一个命名对象(左值),通过常量左值引用const string&)传递给 push_back
  • 特点
    • 不创建临时对象,直接绑定引用到 str
    • 避免拷贝,高效且安全(const 确保不修改原对象)。
 //传递匿名对象(右值)
    push_back(string("222222"));
  • 过程
    1. string("222222") 创建一个匿名 string 对象(右值)。
    2. 该匿名对象通过常量左值引用绑定到 push_back 的参数 s
  • 特点
    • 匿名对象在语句结束后销毁(但因被 const 引用绑定,生命周期延长至函数调用结束)。
    • 仅需一次构造(string 的构造函数),无拷贝。
push_back("222222"):传递字面量(隐式类型转换)
  • 过程
    1. "222222" 是 const char* 类型的字符串字面量。
    2. 隐式转换push_back 期望 const string&,编译器自动调用 string 的构造函数 string(const char*) 创建一个临时 string 对象。
    3. 临时对象通过常量左值引用绑定到 s
  • 特点
    • 隐式创建临时 string 对象,可能导致性能开销(构造 + 析构)。
    • 若 push_back 声明为 push_back(string s)(值传递),会额外触发一次拷贝构造。


网站公告

今日签到

点亮在社区的每一天
去签到