C++ 从 C 到进阶 ④:类和对象(下)—— 构造函数进阶、static、友元与内部类

目录

1.再探构造函数

2.类型转换

3.static成员

[4. 友元](#4. 友元)

5.内部类

[6. 匿名对象](#6. 匿名对象)

[7. 总结](#7. 总结)

1.再探构造函数

  1. 之前我们实现构造函数时,初始化成员变量 主要使⽤函数体内赋值 ,构造函数初始化还有⼀种⽅ 式,就是初始化列表 ,初始化列表的使⽤⽅式是以**⼀个冒号开始**,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
  2. 每个成员变量在初始化列表中只能出现⼀次 ,语法理解上初始化列表可以认为是每个成员变量定义 初始化的地⽅。
  3. 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始 化,否则会编译报错。
  4. C++11⽀持在成员变量声明的位置给缺省值 ,这个缺省值主要是给**没有显⽰**在初始化列表初始化的 成员使⽤的。
  5. 尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没 有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构 造会编译错误。
  6. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。

1.初始化列表的形式:

cpp 复制代码
class Data
{
public:
	Data(int year, int month, int day)    //初始化列表的形式
		:_year(year)                      //以:的形式开始,括号内是初始化的值,括号前是需初始化的变量
		,_month(month)                    //变量与变量之间需以,隔开
		,_day(day)
	{

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

注意:

无参构造函数也是默认构造函数的一种。但如果类中已定义了其他构造函数,编译器就不再生成默认构造函数,此时若还需要无参构造,必须自己写

" : " " , "都是英文符号,注意不要写成中文符号

2.每个成员变量在初始化列表中只能出现⼀次,

cpp 复制代码
class Data
{
public:
	Data(int year, int month, int day)  //相当于对变量定义,定义会开辟空间
		:_year(year)
		,_month(month)
		,_day(day)
	{

	}
private:                                 //对变量进行声明,声明不会开空间
	int _year;
	int _month;
	int _day;
};

3.引用成员变量,const成员变量,没有默认构造的类类型变量

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{

	}
private:
	int _hour;
};

class Data
{
public:
	Data(int year, int month, int day, int ref)
		:_year(year)
		,_month(month)
		,_day(day)
		,_ref(ref)                         //引用的成员变量的初始化的值必须是一个真实存在的实体,
		                                   //不能是临时存在的实体,比如,_ref(1)就会报错
		,_n(2)                             //隐式类型转换,后续会讲
		,_t(3)
	{

	}
private:
	int _year;
	int _month;
	int _day;
	int& _ref;                             //引用成员变量
	const int _n;                          //const成员变量
	Time _t;                               //没有默认构造的类类型成员
};

4.声明的位置给缺省值


cpp 复制代码
class Data
{
public:
	Data(int year, int month, int day, int ref)
	{

	}
private:               //这里不是给了一个定义,而是给了一个缺省值
	                   //给没有初始化列表的成员使用的
	int _year = 1;
	int _month = 1;
	int _day = 1;                          
};

5.

每个构造函数都有初始化列表

每个成员变量都要走初始化列表

1.在初始化列表初始化的成员

2.没有在初始化列表初始化的成员

a.声明的位置有缺省值的用缺省值

b.没有缺省

内置类型:不确定是否初始化,要看编译器,大概率是随机值

自定义类型:有默认构造函数的调用默认构造函数,没有默认构造函数的编译器报错

注意:引用、const成员变量、没有默认构造的类类型成员,只能在初始化列表初始化

6.下面的程序的运行结果:

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出 1 随机值

E. 输出 1 2

F. 输出 2 1

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{

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

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

结果选D,不理解的话看下面的讲解:

2.类型转换

  1. C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  2. 构造函数前⾯加explicit就不再⽀持隐式类型转换。
  3. 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

1.编译器的优化及单参数的隐式类型转换

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a1 = 0)                  //构造函数
		:_a1(a1)
	{
		cout << "A(int a1=0)" << endl;
	}
	A(const A& aa)                  //拷贝构造
	{
		cout << "A(const A& aa)" << endl;
	}
private:
	int _a1;
};

int main()
{
	A aa1 = 1;
}

但是在编译器中遇到这种普通构造+拷贝构造-->直接构造(普通构造),运行后的结果就是:A(int a1=0)

2.C++11以上的支持多参数的隐式类型转换

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	
	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1,int a2)" << endl;
	}
	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;
	}
private:
	int _a1;
	int _a2;
};

int main()
{
	A aa1 = { 1,2 };
	A aa2(aa1);
	return 0;
}

说明: A aa1 = {1,2}; 使用多参数构造函数进行隐式类型转换。A aa2(aa1); 是直接调用拷贝构造函数。

3.取消隐式类型转换(explicit)

cpp 复制代码
explicit A(int a1, int a2)
	:_a1(a1)
	,_a2(a2)
{
	cout << "A(int a1,int a2)" << endl;
}

注意:

  • explicit 修饰的构造函数不能用于隐式类型转换,例如 A aa1 = 1; 或 A aa1 = {1,2}; 都会编译报错。

  • 但可以用于直接初始化,例如 A aa1(1);、A aa1(1,2);、A aa1{1,2}; 都是合法的。

  • explicit 对单参数构造函数和多参数构造函数都适用。

3.static成员

  1. ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
  2. 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  3. ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  4. 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
  5. ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  6. 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量 和静态成员函数。
  7. 静态成员也是类的成员,受public、protected、private访问限定符的限制。
  8. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A()              //默认构造函数
	{
		++_scount;
	}
	A(const A& aa)    //拷贝构造
	{
		++_scount;
	}
	~A()               //析构函数
	{
		--_scount;
	}
	static int GetAScount()
	{
		return _scount;
	}
private:
	static int _scount;  //不能在这里给缺省值进行初始化
};
int A::_scount = 0;      //只能在类外面对类内的静态成员变量进行初始化

//int main()
//{
//	A a1,a2;
//	A a3(a1);
// 
// 两种突破类域的方法:
// 1.对象::静态成员(函数)
// 2.对象.静态成员(函数)
//	cout << A::GetAScount() << endl;
//	cout << a3.GetAScount() << endl;
//}

int main()
{
	A a1,a2;
	{
		A a3(a1);
		cout << A::GetAScount() << endl;
	}
	cout << a1.GetAScount() << endl;
}

4. 友元

  1. 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类 声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
  2. 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  3. 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
  4. ⼀个函数可以是多个类的友元函数。
  5. 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
  6. 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
  7. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  8. 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

1.一个函数是多个类的友元函数

cpp 复制代码
#include<iostream>
using namespace std;

class B;	//这里是对B类进行了前置声明
			//否则下面的友元函数声明中容易找不到B类而报错
class A
{
	//这种就叫做友元函数声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;

};
class B
{
private:
    friend void func(const A& aa, const B& bb);//友元函数(友元类)不受访问限定符的限制
	int _b1 = 3;
	int _b2 = 4;
};

//下面这种类外的函数访问类内的私有成员变量,受到限制
// 就要在类内进行一个函数声明
// 告诉该类:类外的函数可以访问类内的成员变量及函数
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}

int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

可以了解到:func这个函数是A、B两个类的友元函数,同样,一个类也可以是多个类的友元类。

2.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

cpp 复制代码
#include<iostream>
using namespace std;

class A
{
	friend class B;		//B是A类的友元类
						//fun1、fun2是A类的友元函数
						//B类的任何函数都可以访问A的任何成员和函数
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
private:
	int _b1 = 3;
	int _b2 = 4;
public:
	void func1(const A& aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}
	void func2(const A& aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}
};

int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func2(aa);
	return 0;
}

5.内部类

  1. 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  2. 内部类默认是外部类的友元类。
  3. 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
private:
    static int _k;
    int _h = 1;
public:
    // B 放置在 A 内,为 A 的内部类
    // B 是 A 的友元类,B 的成员函数都是 A 的友元函数
    // B 的成员函数可以访问 A 的私有和保护成员
    class B
    {
    public:
        void foo(const A& aa)
        {
            cout << aa._h << endl;   //  可以访问 A 的私有成员
            cout << _k << endl;      //  可以访问 A 的静态成员
        }
        int _bl = 0;
    };

    void Print()
    {
        // 外部类 A 不能访问内部类 B 的私有成员,会报错
        // cout << _bl << endl;      //  错误
        cout << _h << endl;          //  可以访问自己的成员
    }
};

int A::_k = 1;

int main()
{
    A aa;
    A::B b;         // 不能用 B b 直接创建对象,在全局域中找不到 B 类,
                    // 只能先找 A 类,再找 B 类
    b.foo(aa);
    return 0;
}

注意:

_K:静态成员变量,整个程序只存在一份,只属于class A这个类本身,不属于任何一个具体的对象,调用是用的是_K

_h:属于class A这个类的成员变量,可能其他类中也存在_h这个成员变量,即调用时需使用对象._h

6. 匿名对象

  1. ⽤类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象
  2. 匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
cpp 复制代码
#include<iostream>
using namespace std;

class A
{
public:
    A(int a = 0)    // 默认构造函数
        :_a(a)
    {
        cout << "A(int a = 0)" << 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();                // 匿名对象:这一行结束后立即调用析构函数
    // 可以采用上面这样的方式定义对象,这种叫做匿名对象
    // 生命周期只有这一行,结束后就会自动调用析构函数

    // 正常情况下创建对象和调用成员函数需要两行代码
    Solution b;
    b.sum_solution(1);

    // 采用匿名对象只需要一行
    Solution().sum_solution(1);

    return 0;
}

7. 总结

本篇我们深入学习了 C++ 类和对象(下)的核心内容:初始化列表、类型转换、static 成员、友元、内部类和匿名对象。下面是知识点的快速回顾:

知识点 核心作用 要点记忆
初始化列表 成员变量真正初始化的地方 以冒号开始,逗号分隔;引用、const、无默认构造的类类型必须放初始化列表;初始化顺序与声明顺序一致
类型转换 内置类型与类类型之间的隐式转换 单参数构造函数支持隐式类型转换;explicit 可禁止隐式转换;C++11 支持多参数隐式转换
static 成员 属于类而非对象的成员 静态成员变量必须在类外初始化,存放在静态区,所有对象共享;静态成员函数没有 this 指针,不能访问非静态成员
友元 突破封装,访问私有成员 友元函数/友元类在类内声明,加 friend;单向、不可传递;会增加耦合,不宜多用
内部类 定义在另一个类内部的类 独立类,受外部类类域和访问限定符限制;默认是外部类的友元类;外部类对象不包含内部类
匿名对象 临时创建的对象 类型(实参) 定义,生命周期仅当前一行;常用于临时调用成员函数

关键理解:

  • 初始化列表是成员变量定义初始化的地方,即使不写也会走初始化列表;引用、const、无默认构造的类类型必须显式在初始化列表中初始化。

  • 类型转换:单参数构造函数支持内置类型到类类型的隐式转换,explicit 可以禁止这种转换,避免意外的类型转换。

  • static 成员属于整个类,不属于某个对象;静态成员变量在类外初始化,静态成员函数没有 this 指针,只能访问静态成员。

  • 友元提供了一种突破封装的机制,但会破坏封装性,应谨慎使用。

  • 内部类是独立的类,默认是外部类的友元,可以访问外部类的私有成员。

  • 匿名对象生命周期短暂,适合临时使用,可以简化代码。

📌 下篇预告:

《C++ 从 C 到进阶 ⑤:C/C++内存管理》

将深入讲解:

  1. C/C++ 内存分布

  2. C 语言动态内存管理:malloc/calloc/realloc/free

  3. C++ 内存管理方式:new/delete

  4. operator new 与 operator delete 函数

  5. new 和 delete 的实现原理

  6. 定位 new 表达式(placement-new)

  7. malloc/free 和 new/delete 的区别


如果你觉得这篇对你有帮助,欢迎点赞 👍 + 收藏 ⭐,我们下篇见!

相关推荐
stellanke1 小时前
工业化C++:3. 继承、虚函数与运行时多态:从水果加工到日志策略设计
开发语言·c++
垆边人似月.1 小时前
华为题:最多可以参加的会议数目
数据结构·c++·算法
xiangyun611 小时前
C++ list 模拟实现(一):手撕双向链表,从节点到迭代器
数据库·c++·算法·链表·stl·list
别动我齐刘海11 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
程序员老陆11 小时前
C++ std::promise深度解析
c++·程序设计
青少儿编程课堂12 小时前
迪杰斯特拉(Dijkstra)算法:单源最短路与赛场路线规划
c++·python·算法·bfs·信息学竞赛
沫璃染墨13 小时前
《Qt从零入门系列(十三):QSS样式表详解——从选择器与层叠规则到控件美化实战》
开发语言·css·c++·qt·ui·qt6.3·qss
道尔柯南14 小时前
C++ 面向对象编程入门:类和对象(上)
开发语言·c++·算法
aqiu11111116 小时前
【C++算法打怪专栏】C++ `unordered_set` 常用初始化与插入方法总结
开发语言·c++·算法