【C++】类和对象(4)
目录
1. 类型转换
非explicit的单参数构造函数
示例
explicit的单参数构造函数
示例
不同版本的行为
示例 (单参数)
示例(多参数且其余参数有默认值 )
示例(多参数且无默认值)
2. static成员变量
例题1
例题2
3. 友元
4. 内部类
例题
5. 匿名对象
示例
1. 类型转换
C++支持将内置类型隐式转换为类类型对象,需要非explicit的单参数构造函数或者多参数但其余参数有默认值的构造函数。
非explicit的单参数构造函数
示例
假设定义一个 Length 类来表示长度,单位是米,它有一个接受 int 类型参数的构造函数用于设置长度数值:
#include <iostream> using namespace std;class Length { public:// 接受int类型参数的构造函数Length(int meter) : length(meter){}void show() const {cout << "长度为: " << length << " 米" << endl;} private:int length; };int main() { // 复制初始化,将int类型的5转换为Length类型的对象lenLength len = 5; //想要创建一个表示长度为5米的length对象len len.show();return 0; }
这里 int 类型的 5 可以通过 Length 类的构造函数隐式转换为 Length 类型的对象,使用 Length len = 5; 这种复制初始化的方式,代码看起来就像是把一个整数值赋给了 Length 类型的对象,比较符合常规的赋值思维习惯,简单易懂。相比之下,直接初始化 Length len(5); 虽然功能一样,但复制初始化在这种类型转换很自然的场景里,从写法上会给人一种更直观的感觉。
explicit的单参数构造函数
示例
如果构造函数被声明为explicit ,就不再支持隐式类型转换,仅允许直接初始化。
#include <iostream> using namespace std;class MyClass { public: explicit MyClass(int value) // explicit 修饰构造函数,禁止隐式转换: data(value) {} private:int data; };int main() {MyClass obj(10);// 正确,显式调用构造函数 // MyClass obj = 20; 错误! 因为 MyClass 的构造函数是 explicit,禁止隐式转换return 0; }
不同版本的行为
- C++98/03,Length len = 5; 复制初始化的处理方式是:先使用5调用构造函数 Length(int meter) 生成一个临时的Length类型的对象,然后再通过拷贝构造函数将临时对象复制到 len。允许编译器优化省略。
- C++11,优化为直接构造。编译器必须省略临时对象和拷贝构造函数的调用,Length len = 5; 编译器会直接调用构造函数初始化 len,提高了程序效率。
示例 (单参数)
class MyClass
{
public:MyClass(int value) // 非 explicit,允许隐式转换: data(value){} // 无拷贝构造函数(编译器会默认生成)
private:int data;
};void func(MyClass obj)
{/* ... */
}int main()
{MyClass obj = 10; // 隐式转换:int → MyClass//C++98/03行为:先调用构造函数生成临时的MyClass类型对象,再通过拷贝构造函数(若用户未定义,编译器默认生成一个公有的)将临时对象赋值给obj(这里临时对象生命周期仅用于初始化obj,拷贝完成后销毁)//C++11及以后行为:允许直接构造obj而不生成临时对象,不调用拷贝构造函数func(20); // 隐式转换:int → MyClass//C++98/03行为:(两次开销)先生成临时的MyClass类型对象,再通过拷贝构造(若用户未定义,编译器默认生成一个公有的)函数将临时对象传递给func函数的形参obj//C++11及以后行为:(一次开销)允许直接在函数func的形参obj的存储位置(函数栈帧)构造MyClass对象,而不生成临时对象return 0;
}
class MyClass
{
public:MyClass(int value) // 非 explicit,允许隐式转换: data(value){}// 无拷贝构造函数(编译器会默认生成)
private:int data;
};void func(const MyClass& a)//参数为引用类型
{/* ... */
}int main()
{MyClass obj = 10; func(20); //不管是C++98/03还是C++11及以后,当参数是引用类型时,必须创建临时对象,然后绑定到引用上,临时对象生命周期延长至函数结束return 0;
}
示例(多参数且其余参数有默认值 )
#include <iostream>
using namespace std;
class A
{
public:// 多参数但其余参数有默认值的构造函数A(int a, int b = 4) : value(a + b) {cout << value << endl;}private:int value;
};int main()
{ A obj2 = 10; //这里 10 是int类型,隐式转换为A类型,调用多参数(有默认值)构造函数return 0;
}
示例(多参数且无默认值)
#include <iostream>
using namespace std;
class B
{
public:// 多参数且无默认值的构造函数B(int a, int b) : num(a + b) {cout << num << endl;}private:int num;
};int main()
{// 不能隐式转换,显式调用构造函数进行类型转换B obj = B(3, 5);//理论上B(3, 5)复制初始化 先调用构造函数创建临时对象,再调用拷贝构造函数将临时对象赋值给obj,实际上进行了优化//不产生临时对象,也不调用拷贝构造函数,等价于直接初始化 B obj(3,5),二者在效果和性能上完全一致return 0;
}
#include <iostream>
using namespace std;
class Point
{
public:Point(int x, int y) : m_x(x), m_y(y) {}void print() const {std::cout << "(" << m_x << ", " << m_y << ")";}private:int m_x;int m_y;
};void displayPoint(const Point& p)
{p.print();std::cout << std::endl;
}int main()
{// 1.显式构造Point对象后传递displayPoint(Point(10, 20));//Point(10, 20)调用构造函数创建临时对象,这个临时对象直接绑定到函数的参数p上// 2.使用列表初始化语法,编译器隐式调用构造函数displayPoint({ 30, 40 });//编译器看到这种形式,知道接受一个Point类型引用参数,Point类有构造函数,此时编译器会隐式地使用{10,20}作为参数调用构造函数,创建临时对象//3.直接初始化然后再传Point p(5, 5);displayPoint(p);return 0;
}
2. static成员变量
- 用static修饰的成员变量,称之为静态成员变量,静态成员变量必须在在类外进行初始化。
- 所有对象共享同一份静态成员变量,它存储在全局数据区,而不是在每个对象的内存空间中。
- 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
- 静态成员函数中可以访问其他的静态成员,但是不能访问非静态成员,因为没有this指针。
- 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
- 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
- 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
示例:
//实现一个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++_scount;}~A(){--_scount;} //静态成员函数static int GetACount(){//_a++; error! 静态成员函数不能访问非静态成员,因为没有this指针return _scount;}void func(){cout << _scount << endl;cout << GetACount() << endl;}
private:int _a = 4;//类里面声明static int _scount;
};//类外⾯初始化
int A::_scount = 0;int main()
{cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;cout << a1.GetACount() << endl;//cout << A::_scount << endl; 编译报错:error C2248 : “A::_scount” : ⽆法访问private成员(在“A”类中声明) 受访问限定符限制!return 0;
}
例题1
链接[ 求1+2+3+……n]
class Sum
{
public:Sum(){_ret+=_i;++_i;}static int Getret(){return _ret;}
private:static int _i;static int _ret;};int Sum:: _i=1;int Sum:: _ret=0;class Solution {
public:int Sum_Solution(int n) {// Sum a[n]; 变长数组Sum*p=new Sum[n];return Sum::Getret();}
};
例题2
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为(C->A->B->D)
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为(B->A->D->C)
C c; int main() {A a;B b;static D d;return 0; }
解析:
变量c是全局对象,其构造函数最先调用;在main函数中先声明A a; 然后声明B b; 所以A的构造函数先于B调用;D d;是静态局部对象,其构造函数在main函数首次执行到该声明处调用,在A和B之后。所以顺序为:C->A->B->D
非静态局部对象a、b 析构时机是离开main函数作用域时,顺序为构造的反序(B->A);静态对象(全局c和静态局部d)析构时机是程序结束时,顺序为构造的反序(全局C先构造,静态局部D后构造,故析构顺序为D->C)。所以顺序为B->A->D->C
3. 友元
一般来说,类的私有成员外部是不能访问的,而友元函数在一些特殊场景下很有用,比如需要在类外部的函数中访问类的私有数据进行特定操作,又不想把这些数据设为公有成员破坏封装性,就可以将该函数声明为友元函数。
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加 friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
- 友元函数不属于类的成员函数,没有 this 指针。调用时和普通函数一样,直接使用函数名调用。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
- 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
- 一个函数可以是多个类的友元函数。
- 友元提供了便利,但是友元会增加耦合度,破坏了类的封装性,所以友元不宜多用,使用时要谨慎。
#include<iostream>
using namespace std;class B; //前置声明,否则编译器在A类中编译时会因为不知道B是什么类型而报错,告诉编译器B是一个类
class A
{//友元声明
friend void func(const A& aa, const B& bb);
private:int _a1 = 1;int _a2 = 2;
};class B
{
//友元声明
friend void func(const A& aa, const B& bb);
private:int _b1 = 3;int _b2 = 4;
};void func(const A& aa, const B& bb)//func函数可以是多个类的友元函数
{cout << aa._a1 << endl;cout << bb._b1 << endl;
}int main()
{A aa;B bb;func(aa, bb);return 0;
}
当一个类被声明为另一个类的友元类时,那么这个类的所有成员函数都可以访问另一个类的私有和保护成员,无需为每个成员函数单独声明friend关键字。
#include<iostream>
using namespace std;class A
{//友元声明
friend class B;// B整体是A的友元类,友元类的声明是对整个类的授权,B类的所有成员函数都可访问A的私有成员private:int _a1 = 1;int _a2 = 2;
};class B
{
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;}private:int _b1 = 3;int _b2 = 4;
};int main()
{A aa;B bb;bb.func1(aa);bb.func2(aa);return 0;
}
4. 内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在 全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。内部类默认是外部类的友元类。
#include<iostream>
using namespace std;class A
{
private:static int _k;int _h = 1;
public:class B //B默认就是A的友元{public:void foo(const A& a){cout << _k << endl; cout << a._h << endl; }private:int _b = 1;};
};int A::_k = 1; //初始化静态成员变量int main()
{cout << sizeof(A) << endl; //4 非静态成员_hA::B b;//B类的作用域在A类内部,在外部使用B类时需要指定其所属的外部类域A aa;b.foo(aa);return 0;
}
内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
例题
链接[ 求1+2+3+……n]
class Solution
{
private:static int _i;static int _ret;class Sum //内部类{public:Sum(){_ret+=_i;++_i;} };public:int Sum_Solution(int n) {Sum a[n];//变长数组// Sum*p=new Sum[n];// delete []p;return _ret;}};int Solution:: _i=1;int Solution:: _ret=0;
5. 匿名对象
用类型 (实参)定义出来的对象叫做匿名对象,相比之前定义的类型 对象名(实参)定义出来的叫有名对象。
匿名对象通常是临时对象,它们在表达式结束后会被自动销毁。
示例
#include<iostream>
using namespace std;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) {//...return n;}
};int main()
{//有名对象//A aa1(); 不能这么定义对象,因为编译器无法识别这是是⼀个函数声明,还是对象定义 A aa1;cout << endl;A aa2(2);cout << endl; //匿名对象,匿名对象的特点不⽤取名字,但是它的⽣命周期只有这一行,我们可以看到下一行他就会自动调用析构函数A();cout << endl;A(1);cout << endl;//匿名对象在这样场景下就很好⽤:/*Solution sl;cout << sl.Sum_Solution(10) << endl;*///为了更方便cout << Solution().Sum_Solution(10) << endl;return 0;
}
运行结果: