【C++】类和对象(下)

发布于:2025-08-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

一. 再谈构造函数

构造函数里,类初始化成对象有2种方式:构造函数体赋值、初始化列表

之前学的是构造函数体赋值:

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. 类中包含以下成员,必须放在初始化列表位置进行初始化:引用、const、自定义类型成员(且该类没有默认构造函数时)

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

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

为什么设计初始化列表?

引用、const 成员必须在定义的时候初始化:
        引用:必须变成谁的别名
        const:只有1次初始化的机会(定义的时候)

什么是定义的地方?

对象实例化时整体定义。调用构造函数,对每个成员初始化
对象里有多个成员变量,每个成员变量在初始化列表定义

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

class C
{
public:
	C(int c)
		:_c(c)
	{
		cout << "C(int c)" << endl;
	}
private:
	int _c;
};

class B
{
public:
	// B(int a, int ref) 引用的局部变量,出作用域销毁
    B(int a, int& ref) // ref是n的别名
		:_ref(ref) // _ref是ref(n)的别名
		,_n(1)
		,_z(9) // 显示给,不用缺省值
		,_ck(10) // 没有默认构造,必须在这里显示调用构造
		,_ak(99) // 有默认构造也可以显示调(缺省参数)
	{}

private:
	A _aobj; // 有默认构造函数
	A _ak;
	C _ck; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const 
	int _x;
	int _y = 1; // 1是缺省值,是给初始化列表的
	int _z = 1;
};

int main()
{
	// B bb(10, 1);
	int n = 1;
    B bb(10, n);
    return 0;
}

我们不写,内置类型不处理;自定义类型调用(在初始化列表调用)默认构造函数

typedef int DateType;

class Stack
{
public:
	Stack(int capacity = 4) // 构造函数,功能:替代Init
	{
		_a = (DateType*)malloc(sizeof(DateType) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}

	void Push(DateType Date)
	{
		CheckCapacity();
		_a[_size] = Date;
		_size++;
	}

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DateType Top() { return _a[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

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

private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DateType* temp = (DateType*)realloc(_a, newcapacity *
				sizeof(DateType));
			if (temp == nullptr)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_a = temp;
			_capacity = newcapacity;
		}
	}

private:
	DateType* _a;
	int _capacity;
	int _size;
};

class MyQueue
{
public:
	MyQueue() // 不传参就这样写
	{ }

	MyQueue(int capacity) // 自己控制capacity,手动传参
		:_pushst(capacity)
		,_popst(capacity)
	{ }

private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	MyQueue q1; // 调用无参的构造函数
	MyQueue q2(100); // 调用带参的构造函数(构造函数可以构成重载)

	return 0;
}

都会对 _pushst _popst 初始化,所有成员都会走且只能走1次初始化列表因为那是它定义的地方

写了就用你的,不写也会走初始化列表

自定义类型必须调用构造函数,引用、const 必须在定义的时候初始化
        有默认构造函数:我就调默认构造函数,不传参也可以
        没有默认构造函数(eg:只有一个带参的构造函数):在初始化列表时就不知道怎么初始化成员了

所以:没有默认构造函数,编译器不知道怎么初始化成员,就报错了,他要求你来


初始化列表并不能解决所有问题

eg1:要求 检查、初始化数组

class Stack
{
public:
	Stack(int capacity = 10)
		: _a((int*)malloc(capacity * sizeof(int)))
		,_top(0)
		,_capacity(capacity)
	{
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			exit(-1);
		}

		// 要求数组初始化一下
		memset(_a, 0, sizeof(int) * capacity);
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

eg2:动态开辟二维数组

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

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

声明、定义的顺序要保持一致

class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1) // 先走这个
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl; // 1 随机值
	}
private:
	int _a2;
	int _a1;
};

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

explicit关键字

类型转换中间会产生临时变量

int i = 10;
double d = i;

中间会产生 double 类型的临时变量,临时变量再给 d

临时变量又有常性,所以 double& d = i; 错误         const double& d = i; 正确

class A
{
public:
	A(int a)
		:_a(a)
	{ }

private:
	int _a;
};

int main()
{
	A aa1(1); // 调构造
	A aa2 = 2; // 隐式类型转换,整型转化为自定义类型

	return 0;
}

用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)

在同一个表达式内,连续构造会被优化:用 2 直接构造


验证:用 2 直接构造

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 aa2 = 2; // 隐式类型转换,整型转化为自定义类型
	return 0;
}

验证:用 2 调用构造函数,生成 A 类型的临时对象;临时对象再拷贝构造 aa2(构造+拷贝构造)

int main()
{
	A& aa3 = 2; // aa3 引用 aa2 肯定没毛病,但不能引用 2
    // error C2440: “初始化”: 无法从“int”转换为“A &”

    const A& aa3 = 2; // 
    
	return 0;
}

引用不可以;const引用 可以,还调了一次构造:


这个玩法的用处:

#include <string>
//#include <list>

//class string
//{
//public:
//	string(const char* str) // string类中的一个构造函数,可以用一个字符串去构造string类
//	{}
//};

class list
{
public:
	void push_back(const string& str)
	{}
};

int main()
{
	string name1("张三"); // 构造
	string name2 = "张三"; // 构造+拷贝构造+优化
    // 结果一样,但过程不一样 

	list lt1;
	string name3("李四");
	lt1.push_back(name3); // 老老实实构造,你是string,我传string给你

	lt1.push_back("李四"); // 更舒服

	return 0;
}

支持 28行写法的原因:隐式类型转换

"李四"是 const char*,会去调它的构造(string(const char* str)),构造一个临时对象;
临时对象有常性,符合void push_back(const string& str)


如果不想让转换发生呢?

class A
{
public:
	explicit 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 aa2 = 2; // error C2440: “初始化”: 无法从“int”转换为“A”
	const A& aa3 = 2; // error C2440: “初始化”: 无法从“int”转换为“const A &”
	return 0;
}

智能指针就不想发生转换

二. static 成员

想统计 A 创建了多少个对象

先想到定义全局变量

int _scount = 0;

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

A aa0;

A Func(A aa1)
{
	cout << __LINE__ << ":" << _scount << endl;
	return aa;
}

int main()
{
	cout << __LINE__ << ":" << _scount << endl;
	A aa1;
	static A aa2;
	Func(aa1);
	cout << __LINE__ << ":" << _scount << endl;

	return 0;
}

输出结果:24:1         18:4         28:3

24行的那一个对象是 aa0
说明:全局对象在 main 函数之前就会调用构造;局部的静态对象不会在 main 函数之前初始化

到 18 行,有 aa0 aa1 aa2,还有一个是自定义类型传参调用的拷贝构造

传值返回,27行结束就销毁了

A aa0;

void Func()
{
	static A aa2;
	cout << __LINE__ << ":" << _scount << endl;
}

int main()
{
	cout << __LINE__ << ":" << _scount << endl; // 1
	A aa1;
	
	Func(); // 3
	Func(); // 3

	return 0;
}

aa2 是局部的静态对象,不在函数栈帧里,在静态区,只会定义 1 次

祖师爷不喜欢这种方式,不想让你随便访问,提出封装的方式


2.1 概念

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

2.2 特性

  1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
  6. 静态成员变量不能给缺省值。缺省值是给初始化列表用的,它没有初始化列表
class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }

    // 3.没有this指针,指定类域和访问限定符就可以访问
	static int GetACount()
    {
    	// _a1++; 错! 5.静态里,不能访问非静态的,因为没有this指针
    	return _scount; 
    }

private:
	// 成员变量 -- 属于每个一个类对象,存储对象里面
	int _a1 = 1;
	int _a2 = 2;

	// 静态成员变量 -- 1.属于类,属于类的每个对象共享,存储在静态区(生命周期是全局的)
	static int _scount;
};

// 2.全局位置,类外面定义。不能在初始化列表定义,因为它不是对象自己的成员
int A::_scount = 0;

A aa0;

void Func()
{
	static A aa2;
	cout << __LINE__ << ":" << aa2.GetACount() << endl; // 3.对象.静态成员
}

int main()
{
	cout <<__LINE__<<":"<< A::GetACount() << endl; // 3.类名::静态成员
	A aa1;
	
	Func();
	Func();

	return 0;
}

因为是私有,上面就不能直接访问 _scount,只能通过公有的成员函数

静态成员变量和静态成员函数一般都是配套出现


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

class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }

	void Func1()
	{
		// 非静态能否调用静态:可以:没有类域、限定符限制
		GetACount();
	}

	void Func2()
	{
		++_a1;
	}

	static int GetACount()
	{
		// 静态能否调用非静态:不可以:非静态的成员函数调用需要this指针,我没有this
		Func2();

		// _a1++; 静态里,不能访问非静态的,因为没有this指针
		return _scount; 
	}

private:
	// 成员变量
	int _a1 = 1;
	int _a2 = 2;

	// 静态成员变量
	static int _scount;
};

一个用 static 的绝佳场景(一种思想):

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

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

调用这个成员函数需要对象,但这个函数是为了获取对象(先有鸡还是蛋的问题)用 static 可以解决

三. 友元

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

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

1. 友元函数

我的函数声明成你的朋友,我在类外面就可以访问你的私有

说明:

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

在类外面有时需要用对象访问成员。

以前访问的方式是成员函数,成员函数的第一个位置都是this指针

下面的代码要符合用法,流对象 ostream的cout对象 和 istream的cin对象 要抢占左操作数。写成成员函数会抢位置,所以不能写成成员函数(详解见上一篇文章的 流插入打印、流提取 )【C++】类和对象(中)拷贝构造、赋值重载_c++构造值一样的对象-CSDN博客

        Date.h

class Date
{
	// 友元函数声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1); // 构造 

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

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

ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

        Date.cpp

Date::Date(int year, int month, int day) // 构造
{
	if (month > 0 && month < 13
		&& day > 0 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}
}

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

istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >> day;

	if (month > 0 && month < 13
		&& day > 0 && day <= d.GetMonthDay(year, month))
	{
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}

	return in;
}

友元提供了便利,增加了耦合(关联度)

eg:不想叫_year,想叫year,类里面得改,友元函数里也得改

    2. 友元类

    我的类成为你的友元,在我整个类里面,可以随便访问你的私有、保护

    说明:

    • 友元关系是单向的,不具有交换性

                    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行

    • 友元关系不能传递

                    如果B是A的友元,C是B的友元,则不能说明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;
    };

    四. 内部类

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

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

    特性:

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

    定义出来给别人用的东西(目前学的:成员变量,成员函数,内部类)才会收到访问限定符的限制


    class A
    {
    private:
        static int k;
        int h;
    public:
        class B
        {
        public:
            void foo()
            { }
        private:
            int b;
        };
    };
    
    int A::k = 1;
    
    int main()
    {
        cout << sizeof(A) << endl; // 4
    
        A aa;
        A::B bb; // 如果 B是私有的,这样就错
    
    	return 0;
    }

    k 没有存在对象里,所以不计算 k 的大小
    A类 里面没有创建 B对象,所以不算 b 的大小

    class A
    {
    private:
        static int k;
        int h;
    public:
        class B
        {
        public:
            void foo()
            { }
        private:
            int b;
        };
    
        B _bb; // A类 里,用B类 创建了对象 _bb。要算 _bb的大小
    };
    
    int A::k = 1;
    
    int main()
    {
        cout << sizeof(A) << endl; // 8
    	return 0;
    }

    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
    		}
    	};
    };
    
    int A::k = 1;
    
    int main()
    {
    	A::B b;
    	b.foo(A());
    
    	return 0;
    }

    五. 匿名对象

    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)
        {
            cout << "Sum_Solution" << endl;
            //...
            return n;
        }
    };
    int main()
    {
        A aa(1); // 有名对象 -- 生命周期在当前函数局部域
        A(2); // 匿名对象 -- 生命周期在当前行
              // 后面没人用,干脆直接销毁
    
    // 想调用Sum_Solution函数
    // 1.有名对象
        Solution sl;
        // 不能加():Solution sl(); // 不知道是对象还是函数名
        sl.Sum_Solution(10);
    
    // 2.匿名对象
        Solution().Sum_Solution(20);
        // 必须加()
        // Solution.Sum_Solution(20); // 错,必须是 "对象." 要传this
    
        // Solution::Sum_Solution(20); // 错,只有静态成员函数才能这么调,因为没有this指针
    
    
        // A& ra = A(1); // 错,匿名对象具有常性
        const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域
                            // ra还要用,留下来
    
        Solution().Sum_Solution(20);
        return 0;
    }


    void push_back(const string& s) // 如果不加const,仅第一个可以编过
    {
        cout << "push_back:" << s << endl;
    }
    
    int main()
    {
        string str("11111"); // 有名对象
        push_back(str);
    
        push_back(string("222222")); // 匿名对象。传上去const引用,延长生命周期
    
        push_back("222222"); // 临时对象。隐式类型转换(详解见上文explicit关键字)
    
        return 0;
    }

    六. 拷贝对象时的一些编译器优化

    class A
    {
    public:
        A(int a = 0)
            :_a(a)
        {
            cout << "A(int a)" << endl;
        }
    
        A(const A& aa)
            :_a(aa._a)
        {
            cout << "A(const A& aa)" << endl;
        }
    
        A& operator=(const A& aa)
        {
            cout << "A& operator=(const A& aa)" << endl;
    
            if (this != &aa)
            {
                _a = aa._a;
            }
    
            return *this;
        }
    
        ~A()
        {
            cout << "~A()" << endl;
        }
    private:
        int _a;
    };
    void Func1(A aa)
    {}
    
    void Func2(const A& aa)
    {}
    
    int main()
    {
        A a1;
        Func1(a1); // 传值传参:a1传给aa,要调用拷贝构造
        Func2(a1); // 没有拷贝构造
        return 0;
    }

    void Func1(A aa)
    {}
    
    void Func1(const A& aa) // 类型不一样,构成重载
    {}
    
    int main()
    {
        A a1;
        Func1(a1); // “Func1”: 对重载函数的调用不明确 <==> 无参、全缺省
        return 0;
    }

    A Func3()
    {
        A aa;
        return aa;
    }
    
    int main()
    {
        Func3();
        return 0;
    }

    旧编译器:传值返回,返回aa的拷贝        

    新编译器:连续的构造+拷贝 ==> 直接构造        

    A& Func4() // 只有静态,出了作用域没有销毁(*this)才能用引用返回
    {
        static A aa;
        return aa;
    }
    
    int main()
    {
        Func4();
        return 0;
    }

    传引用返回,没有拷贝


    A Func5()
    {
        A aa;
        return aa;
    }
    
    int main()
    {
        Func5(); // 这个表达式返回的值是aa拷贝的临时对象
        // 所以不能这样接收:A& ra = Func5();
        const A& ra = Func5();
    
        return 0;
    }

    A Func5()
    {
        A aa;
        return aa;
    }
    
    int main()
    {
        A ra = Func5();
        return 0;
    }

    同一行连续的一个步骤里,>=2个 构造\拷贝构造\构造+拷贝构造 有可能优化

    void Func1(A aa)
    {}
    
    A Func5()
    {
        A aa;
        return aa;
    }
    
    int main()
    {
        A ra = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造
    
        A aa1;
        Func1(aa1); // 不优化。在2行
    
        Func1(A(1)); // 构造+拷贝构造 --> 优化为构造
    
        Func1(1); // 构造+拷贝构造 --> 优化为构造
        A aa2 = 1; // 构造+拷贝构造 --> 优化为构造
        // 19.20 行等价
        return 0;
    }

    int main()
    {
        A ra1 = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造
    
        cout << "==============" << endl;
    
        A ra2;
        ra2 = Func5(); // 不会优化。对象已经定义出来了;而且这里不是拷贝构造,是赋值
    
        return 0;
    }

    构造、拷贝构造尽量写到1个步骤里

    本篇的分享就到这里了,感谢观看,如果对你有帮助,别忘了点赞+收藏+关注
    小编会以自己学习过程中遇到的问题为素材,持续为您推送文章