C++类和对象(三)
希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!
目录
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){ }
需要注意的是:
- 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量 ,const成员变量,自定义类型成员(且该类没有默认构造函数时)。
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。 也就说,不管写不写初始化列表,在对象创建的时候,都会经过初始化列表。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
- 函数体和初始化列表都是构造函数的一部分,初始化列表的存在可以分担一些初始化的操作,但是在一些情况下不能独立完成初始化操作。
注意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; // 5cout << __LINE__ << ": " << _count << endl; // 5return bb4; // 销毁 bb bb4 3
}
B bb1; // 1int main()
{cout << __LINE__ << ": "<<_count << endl;B bb2; // 2static B bb3; // 3cout << __LINE__ << ": " << _count << endl;B bb5 = Func(bb3); //4cout << __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;
静态成员变量属于类本身,并非类的某个对象,所以它需要在类外进行单独定义,从而为其分配内存空间。
定义位置不受访问限定符的限制
无论静态成员变量在类中被声明为
private
、protected
还是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.友元函数
特点:
- 非成员函数:友元函数不属于类,定义时不需要加类作用域前缀(如
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. 内部类
内部类(嵌套类) 是定义在另一个类内部的类。
特性
- 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
- 内部类就是外部类的友元类;
- 内部类可以在外部类的public、protected、private任何部分。
内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。 sizeof(外部类)=外部类,和内部类没有任何关系。内部类独立存在:外部类和内部类的对象在内存中是分离的,彼此不包含对方。
class A
{
private:static int k;int h;
public:class B // B天生就是A的友元{public:void Func(const A& a){cout << k << endl;//OKcout << 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"));
- 过程:
string("222222")
创建一个匿名string
对象(右值)。- 该匿名对象通过常量左值引用绑定到
push_back
的参数s
。- 特点:
- 匿名对象在语句结束后销毁(但因被
const
引用绑定,生命周期延长至函数调用结束)。- 仅需一次构造(
string
的构造函数),无拷贝。
push_back("222222"):传递字面量(隐式类型转换)
- 过程:
"222222"
是const char*
类型的字符串字面量。- 隐式转换:
push_back
期望const string&
,编译器自动调用string
的构造函数string(const char*)
创建一个临时string
对象。- 临时对象通过常量左值引用绑定到
s
。- 特点:
- 隐式创建临时
string
对象,可能导致性能开销(构造 + 析构)。- 若
push_back
声明为push_back(string s)
(值传递),会额外触发一次拷贝构造。