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

一. 再谈构造函数

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

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

cpp 复制代码
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、自定义类型成员(且该类没有默认构造函数时)

cpp 复制代码
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次初始化的机会(定义的时候)

什么是定义的地方?

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

cpp 复制代码
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;
}

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

cpp 复制代码
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:要求 检查、初始化数组

cpp 复制代码
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:动态开辟二维数组

cpp 复制代码
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. 成员变量 在类中声明次序 就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

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

cpp 复制代码
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关键字

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

cpp 复制代码
int i = 10;
double d = i;

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

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

cpp 复制代码
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 直接构造

cpp 复制代码
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(构造+拷贝构造)

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

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

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


这个玩法的用处:

cpp 复制代码
#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)


如果不想让转换发生呢?

cpp 复制代码
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 创建了多少个对象

先想到定义全局变量

cpp 复制代码
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行结束就销毁了

cpp 复制代码
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. 静态成员变量不能给缺省值。缺省值是给初始化列表用的,它没有初始化列表
cpp 复制代码
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. 非静态成员函数 可以调用 类的静态成员函数 吗?

cpp 复制代码
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 的绝佳场景(一种思想):

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

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

cpp 复制代码
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

cpp 复制代码
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

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的友元

  • 友元关系不能继承,在继承位置再给大家详细介绍
cpp 复制代码
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. 受访问限定符的限制

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


cpp 复制代码
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 的大小

cpp 复制代码
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;
}

cpp 复制代码
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;
}

五. 匿名对象

cpp 复制代码
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;
    }
};
cpp 复制代码
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;
}

cpp 复制代码
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;
}

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

cpp 复制代码
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;
};
cpp 复制代码
void Func1(A aa)
{}

void Func2(const A& aa)
{}

int main()
{
    A a1;
    Func1(a1); // 传值传参:a1传给aa,要调用拷贝构造
    Func2(a1); // 没有拷贝构造
    return 0;
}
cpp 复制代码
void Func1(A aa)
{}

void Func1(const A& aa) // 类型不一样,构成重载
{}

int main()
{
    A a1;
    Func1(a1); // "Func1": 对重载函数的调用不明确 <==> 无参、全缺省
    return 0;
}

cpp 复制代码
A Func3()
{
    A aa;
    return aa;
}

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

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

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

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

int main()
{
    Func4();
    return 0;
}

传引用返回,没有拷贝


cpp 复制代码
A Func5()
{
    A aa;
    return aa;
}

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

    return 0;
}
cpp 复制代码
A Func5()
{
    A aa;
    return aa;
}

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

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

cpp 复制代码
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;
}
cpp 复制代码
int main()
{
    A ra1 = Func5(); // 拷贝构造+拷贝构造 --> 优化为拷贝构造

    cout << "==============" << endl;

    A ra2;
    ra2 = Func5(); // 不会优化。对象已经定义出来了;而且这里不是拷贝构造,是赋值

    return 0;
}

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

本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注

小编会以自己学习过程中遇到的问题为素材,持续为您推送文章