前言
本篇博客讲解一下c++的类和对象,看这个之前请先看:C++入门基础-CSDN博客
💓 个人主页:普通young man-CSDN博客
⏩ 文章专栏:C++_普通young man的博客-CSDN博客
⏩ 本人giee:普通小青年 (pu-tong-young-man) - Gitee.com
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章
目录
再探构造函数(初始化列表)
初始化列表的语法
初始化列表以一个冒号
:
开始,后跟一个由逗号,
分隔的成员变量列表,每个成员变量后面跟着一个括号内的初始值或表达式。例如:
class DATA2 { public: DATA2():_year(2), _month(2), _day(2)//初始化列表 { } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; };
成员变量初始化的规则
- 引用成员 和 const成员 必须在初始化列表中初始化。
- 无默认构造函数的类类型成员 也必须在初始化列表中初始化。
- 成员变量可以在声明时给出默认值,用于未在初始化列表中显式初始化的成员。
class DATA3 { public: DATA3(int& d) :_year(2), _month(2), _day(2), _t1(10), _ret(d) _c(20)//初始化列表 { //_c = 20; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; //增加自定义类型 Time _t1; //const修饰 const int _c; //引用 int& _ret; };
如果我不在初始化列表,初始化这个三个自定义类型,就会报错
引用和引用必须在构造函数之前初始化(初始化列表是在构造函数之前初始化),而自定义类里面不是默认构造。
初始化列表与成员变量的初始化顺序
成员变量在初始化列表中的初始化顺序遵循它们在类中声明的顺序,而不是在初始化列表中出现的顺序。因此,为了清晰和减少混淆,建议将成员变量在类中的声明顺序和初始化列表中的顺序保持一致。
构造函数初始化列表的好处
- 效率:直接初始化比先默认构造再赋值更高效,尤其是对于复杂对象。
- 安全性:确保了所有成员变量都得到初始化,避免了未初始化的成员可能引发的问题。
- 明确性:使构造函数逻辑更加清晰,易于阅读和维护。
还有一个知识,就是可以在内置类型当中给缺省值,这个是c++11支持:
class MyClass {
private:
int _year = 1; // 默认值为1
int _month = 1; // 默认值为1
int _day{}; // 使用统一初始化语法,等同于默认构造
Time _t = Time(1); // 假设Time有一个接受整数参数的构造函数
const int _n = 1; // const成员变量的默认值
int* _ptr = nullptr; // 使用nullptr初始化指针,C++11的特性
};
类型转换
C++ 允许从内置类型(如 int, float 等)到类类型的隐式类型转换,但这要求类中必须定义一个接受相应内置类型参数的构造函数。为了防止不必要的隐式转换,可以在构造函数声明前使用
explicit
关键字,这样编译器就不会自动执行这类转换,从而避免潜在的错误和混淆。简而言之,在 C++ 中:
- 如果类有一个单参数构造函数,且参数类型是内置类型,那么从该内置类型到类类型的隐式转换是默认允许的。
- 若要禁用此类隐式转换,只需在构造函数前添加
explicit
关键字即可。这会阻止编译器在没有显式调用构造函数的情况下自动进行类型转换。
//隐式转换
class IN
{
public:
//不加explicit从double隐式转换int
IN(int a) {
_a = a;
}
//加explicit
/*explicit IN(int a) {
_a = a;
}*/
void Print() {
cout << _a << endl;
}
private:
int _a;
};
int main() {
double d = 2.5;
IN s1 = d;
s1.Print();
return 0;
}
运行一下加了explicit后的报错:
static成员
静态成员变量
- 定义与初始化:静态成员变量为所有类的所有对象所共享,它们不属于任何一个具体对象,而是属于类本身。因此,静态成员变量必须在类外进行初始化,即便在类内部声明。
- 存储位置:静态成员变量存储在全局内存区域的静态存储区中,而非在堆栈或堆中为每个对象分配空间。
- 访问方式:静态成员变量可以通过类名直接访问,如
ClassName::staticVariable
,也可以通过对象访问,尽管它不依附于任何对象。- 访问控制:静态成员变量同样受到
public
、protected
和private
访问限定符的约束,这决定了它们可以从何处被访问。- 初始化限制:静态成员变量不能在声明位置直接初始化,因为它们不受构造函数初始化列表的影响,而是需要在类外单独初始化。
class STatic { public: STatic() { }; private: //类里面声明 static int _i; }; //类外初始化 static int _i = 1;
静态成员函数
- 没有
this
指针:静态成员函数不拥有this
指针,这意味着它们无法访问非静态成员变量和调用非静态成员函数,因为这些都需要通过this
指针来引用具体的对象实例。- 访问权限:静态成员函数可以访问类中的所有静态成员变量和其他静态成员函数,但不能访问非静态成员。
- 调用方式:静态成员函数同样可以通过类名直接调用,如
ClassName::staticFunction()
,或者通过对象调用,尽管它们不依赖于任何对象实例。class STatic { public: static void swap(); void Print() { cout << _i << endl; } private: //类里面声明 static int _i; }; //类外初始化 int STatic::_i = 1; void STatic::swap() { cout << "swap" << endl; } int main() { STatic s1; s1.Print(); //非静态可以调用动态成员 s1.swap(); }
欧克,现在我已经说完了,接下做一道牛客题来直观体验:求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
class Sum{ public: Sum(){ _ret += _i;//1+0 = 1 1+1 = 2 2+2 = 4 ++_i;//i每次++ } static int getret(){//static一个函数来存储构造的返回 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 arr[n];//变长数组 return Sum::getret(); } };
友元
友元函数
- 定义与声明:友元函数在类的内部声明,但在类的外部定义。其声明前需要加上
friend
关键字。- 位置灵活:友元函数可以在类的任何位置声明,不受public、protected或private访问限定符的限制。
- 非成员身份:友元函数不是类的成员函数,它没有this指针,因此不能通过对象调用。
- 多重友元:一个函数可以同时作为多个类的友元。
友元类
- 成员函数访问权限:当一个类被声明为另一个类的友元时,该友元类的所有成员函数都获得了访问另一个类私有和保护成员的权限。
- 单向关系:友元关系是不对称的,即如果类A是类B的友元,这并不意味着类B也是类A的友元。
- 不可传递性:友元关系不会传递。即使类A是类B的友元,而类B又是类C的友元,这并不意味着类A自动成为类C的友元。
使用考量
- 便利性与耦合度:友元机制虽然增加了代码设计的灵活性,但同时也可能引入不必要的耦合,降低类之间的独立性。
- 封装破坏:过度使用友元可能会破坏封装原则,使得原本应该隐藏的内部实现变得暴露无遗。
class f2;//前置声明,不然f1中的友元声明不认识f2
class f1
{
friend void func(const f1& a, const f2& b);
public:
private:
int _a = 20;
};
class f2
{
friend void func(const f1& a, const f2& b);
public:
private:
int _a = 10;
};
void func(const f1& s1, const f2& s2) {
cout << s1._a << s2._a << endl;
}
int main() {
f1 s1;
f2 s2;
func(s1, s2);
}
类与类之间(舔狗和你女朋友之间)
class f1
{
friend class f2;
public:
private:
int _a = 20;
};
class f2
{
public:
void func(const f1& s1) {
cout << s1._a << endl;//创建了友元所以可以访问f1
}
private:
int _a = 10;
int _b = 20;
};
int main() {
f1 s1;
f2 s2;
s2.func(s1);
}
如果你用f1访问f2,就不行,因为你不在他的好友圈,他在你的好友圈,所以它可以随意动你的东西,这不就是现实舔狗!!!
内部类
独立性:尽管内部类是在外部类的范围内定义的,但它是一个完全独立的类。这意味着它可以有自己的方法、属性和构造函数。
访问控制:内部类可以访问外部类的所有成员,包括私有(private)、受保护(protected)或默认访问级别的成员。这是因为内部类实际上可以被视为外部类的一个成员。同时,外部类也可以通过创建内部类的实例来访问内部类的成员。
封装:当一个类A与另一个类B有非常紧密的联系时,将A作为B的内部类可以提高代码的封装性和模块化。这使得A成为B的“专属”类,限制了对A的访问,只有B可以直接使用A,而其他部分的代码无法直接访问到A,除非它们也通过B来间接访问。
访问限定符:内部类可以被声明为
public
、protected
、private
或默认(没有访问修饰符)。如果内部类被声明为private
或protected
,那么它只能被外部类或同一包内的其他类访问。友元关系:内部类默认被认为是外部类的“友元”类,这意味着内部类可以访问外部类的所有成员,即使它们被标记为私有。但需要注意的是,“友元”这个概念在Java中并不像在C++中那样正式存在,而是通过访问权限来实现类似的效果。
实例关系:每个内部类的实例都与一个外部类的实例相关联。这意味着,当你创建一个内部类的实例时,必须先有一个外部类的实例。然而,静态内部类(Static Inner Class)是一个例外,它不需要外部类的实例就可以创建。
直接优化一下前面的那一道题:
class Solution {
private:
static int _i;
static int _ret;
class Sum{
public:
Sum(){
_ret += _i;//1+0 = 1 1+1 = 2 2+2 = 4
++_i;//i每次++
}
static int getret(){//static一个函数来存储构造的返回
return _ret;
}
};
public:
int Sum_Solution(int n) {
Sum arr[n];//变长数组
return Sum::getret();
}
};
int Solution::_i = 1;
int Solution::_ret = 0;
他是我的友元,我是不是就可以直接访问,就不需要类访问,两种方法性能一样,只是说方便很多。
匿名对象
有名对象:这是最常见的对象创建方式,其中对象不仅被实例化,还被赋予了一个引用名称。例如,在Java中,
Type objectName = new Type(args);
这样的语句创建了一个Type
类型的对象,并将其引用存储在objectName
变量中。后续可以通过这个变量名访问和操作对象。匿名对象:这种对象在创建时并没有被赋予一个引用名。它通常用于只需要对象执行一次任务的情况,例如调用一个方法后即丢弃。在Java中,创建匿名对象的语法看起来像这样:
new Type(args).method();
在这里,Type
对象被创建并立即调用了method()
,之后对象的引用将不再被持有,因为没有变量存储这个引用。这意味着匿名对象的生命周期仅限于当前行代码的执行。使用匿名对象的优点在于它可以帮助减少代码的冗余和复杂性,特别是在对象仅用于单一目的的情况下。由于没有持久的引用,匿名对象在使用后会被垃圾回收机制自动清理,有助于节省内存资源。
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();
// 但是我们可以这么定义匿名对象,匿名对象的特点不⽤取名字,
// 但是他的⽣命周期只有这⼀⾏,我们可以看到下⼀⾏他就会⾃动调⽤析构函数
A();
A(1);
A aa2(2);
// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使⽤场景,这个我们以后遇到了再说
Solution().Sum_Solution(10);
return 0;
}
这个代码写了一个构造函数和一个析构函数,你可以调试看一下,就会发现为什么他的生命周期很短,为什么是一次性,它构造后,返回到当前位置,然后他又会去调用析构。
这个匿名,其实就是方便了