很高兴和大家见面,给生活加点impetus!!开启今天的编程之路
今日学习类和对象,翻过这道坎,C++就不怕啦!!
作者:٩( ‘ω’ )و260
我的专栏:C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!!
类和对象
再探构造函数
之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
1:初始化列表冒号开始,中间逗号分隔,用括号中的内容给成员变量赋值
我们可以先来简单的写一个初始化列表:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)//初始化列表
{
//函数体
}
2:每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方
如果定义两次,就会报错!!
Date(int year=1,int month=1,int day=1)
:_year(year)
,_year(year)
,_month(month)
,_day(day)
{
}
其实因为初始化列表就是函数定义的地方,定义两次的话当然就会报错重定义。
3:有些变量必须在初始化列表进行初始化,例如:引用成员变量,const成员变量,没有默认构造的类类型变量。
如下:
class Date
{
public:
Date(const int& x ,int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
, b(x)
, a(10)
{
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
//const型
const int a;
//引用
const int& b;
};
int main()
{
int x = 0;
Date d1(x);
d1.Print();
return 0;
}
如果我们放在函数体中初始化,我们再来看看报错信息:
{
a = 10;
}
未初始化的报错信息
4:C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的
来看下方代码:
class Date
{
public:
Date( const int& x )
: b(x)
{
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year=1;
int _month=1;
int _day=1;
//const型
const int a=10;
//引用
const int& b;
};
int main()
{
int x = 0;
Date d1(x);
d1.Print();
return 0;
}
注意:private中只是给成员变量声明,并没有给定义,只有在初始化列表的时候,这些成员函数会被定义。
我的建议是引用变量在初始化列表中定义初始化,因为即可以接收外来的参数,况且在私有成员变量之中引用参数也不好给缺省值
一定要在私有成员变量的位置给引用参数缺省值,那就这样写:
int c = 0;
class A {
private:
const int& _a = c;
};
但从可视化的角度来说,不如直接传参的那种形式更好
5:尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的⾃定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误
因为初始化列表是成员变量定义的地方,私有中成员函数就只是声明,以后能用初始化列表初始化的地方就不要再用函数体了,除非只能用函数体来进行初始化,例如:
class A{
public:
private:
int* ptr=(int*)malloc(sizeof(int)*n);
};
比如给了缺省值,想要给这个数组赋值,那肯定只能够通过for循环或者memset给数组赋值,数组的初始化肯定是怒能够放在初始化列表的。
6:初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
为了更好说明,我们来看一道例题:
一定要注意是先初始化先声明的成员变量,就是先初始化_a2,再初始化_a1,但是先初始化a2的时候是_a1随机值,初始化_a1就是传过来的实参,所以结果选择D
所以为了可视化,代码的声明和初始化的顺序必须相同!!
总结,初始化列表重要的集中初始化方式:
无论是否显示写初始化列表,每个构造函数都有初始化列表;
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
类型转换
我们先来说明结论:
1:C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
2:构造函数前面加explicit就不再支持隐式类型转换。
3:类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
我们c语言学习的时候就明白,比如需要将浮点型强制类型转换为整形会创建临时变量,因为内置类型的运算符编译器能够自己实现,但是类不行,所以我们需要自己实现这个过程。
来看示例代码:
class A {
public:
A(int a)
:_a(a){}
private:
int _a;
int _b;
int _c;
};
int main()
{
A a = 10;//隐式类型转换,并不会报错
return 0;
}
这里编译器会现将10构造初始化为一个类,随后拷贝构造给类a,这样,a就能够被初始化成功。
或者这句隐式类型转换可以理解为:
A a(10);
思路:10构造临时对象,临时对象拷贝构造a,优化后直接构造,没有拷贝构造
但是如果我们将构造函数屏蔽掉:
//A(int a)
// :_a(a){}
就会:
为什么呢?
因为我将构造函数给屏蔽掉了,前面我们提到,编译器默认生成的构造函数是不能初始化内置类型的,而编译器默认生成的拷贝构造是可以对内置类型进行浅拷贝的,这也是为什么我们这里没有写拷贝构造也能够实现隐式类型转换。总之,隐式类型转换构造函数和拷贝构都必须存在而且对对象必须能够发挥作用
因为是隐式,又是我们并不能够发现,如果为了防止隐式类型转换,在构造函数的前面加上explicit,这样我们只能够显示类型转换:
class Test
{
public:
explicit Test(int i);
};
Test t2 = 1;//编译报错
Test t2(2);//编译没问题
此时只能够直接构造常量或者拷贝构造已经实例化的对象
在C++11之后,我们能够实现隐式类型转换多个参数,具体写法如下:
class A {
public:
A(int a,int b)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
int _c;
};
int main()
{
A a = { 10 ,20};
return 0;
}
编译没问题,如果需要隐式类型转换多个参数,根据参数的个数,只需将待隐式类型转换的参数利用大括号括起来即可。
1:为了不必要的隐式类型转换从而造成巨大错误,我的建议是成员函数中加上explicit
2:当多种类型转换可能同时发生时,编译器会根据转换的优先级来选择合适的转换。如果编译器无法明确选择一种转换,则会引发编译错误。例如强制类型转换,隐式类型转换,显示类型转换等
static成员
用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
例如:
class A {
public:
A(int a,int b,int c)
:_a(a)
,_b(b)
,_c(c)
{}
private:
int _a;
int _b;
int _c;
};
static int a = 10;
但是这样就会出现一个问题,现在是全局变量,全局变量的缺点很多,比如:
1:多个头文件包含会编译错误
2:全局变量容易被修改,容易被访问到等
所以我们将该静态变量放入到成员变量中,这样就能够实现封装的作用,然后再来初始化
private:
int _a;
int _b;
int _c;
static int a;
};
int A::a=10;
2:静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区
就相当于是我们每个类都能够访问到这个静态成员变量,因为是存放在静态区的,并不是存放在类域之中的。
如果此时是公有的成员变量,访问方式如下:
int main()
{
A b;
A::a;//第一种访问方式
b.a;//第二种访问方式
return 0;
}
利用类名或者实例化对象访问,因为编译器默认只会去局部域和全局域查找,并不会去类域中去查找。如果不是公有的,类外中是访不到的,只能通过类中成员函数才能访问的到
3:用static修饰的函数叫做静态成员函数,静态成员函数只能访问静态成员变量,非静态成员函数静态和非静态成员变量都可以。
static int GetA()//静态成员函数
{
return a;
}
但是如果我们利用这个函数去访问其他内置类型非静态成员变量,就会报错
所以我的建议是尽量使用非静态成员函数,因为两者都可以访问
那么为什么这种方式会报错呢?或者我们就是要用静态成员函数访问非静态成员变量,那该怎么做呢?
4:用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
前面我们就已经说了,静态成员变量被所有类共享,那么请问,如果有this指针,那么这个this指针该指向那个类实例化的对象呢?所以就直接没有this指针,没有this指针,想要静态成员函数访问类中某个非静态成员变量就只能手动添加上参数
例如改进上面的代码:
class A {
public:
A(int a,int b,int c)
:_a(a)
,_b(b)
,_c(c)
{}
static int GetA(const A& a)//接收这个对象,引用减少拷贝,提高效率
{
return a._b;//返回对象中的成员变量
}
private:
int _a;
int _b;
int _c;
static int a ;
};
int A::a = 10;
int main()
{
A a1(2025, 3, 13);//构造函数初始化
a1.GetA(a1);//没有this指针,只能手动传递这个对象
return 0;
}
静态成员也是类的成员,受public、protected、private 访问限定符的限制
主要是为了满足C++封装的目的,正是因为被封装,所以突破类域就可以访问静态成员,可以通过类名 :: 静态成员 或者 对象 . 静态成员来访问静态成员变量和静态成员函数。
5:静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表
简洁点来说,初始化列表是成员变量定义的地方,此时就会在类域中创建空间,可以静态变量都不属于一个类,那么如果走了,那么是不是代表静态变量的空间开辟在类域中了?显然这是不合理的,所以静态变量声明的时候不要给缺省值。
来看代码验证:
友元
上面我们提到了在类外访问私有成员变量的几种方法,其实友元也能够访问到私有成员函数。
1:友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面
例如:
class A
{
friend void Print(const A& d);
public:
A(int a = 10, int b = 20, int c = 30)
{
_a = a;
_b = b;
_c = c;
}
private:
int _a;
int _b;
int _c;
};
void Print(const A&d)
{
cout << d._a << " " << d._b << " " << d._c << endl;
}
int main()
{
A a(2025, 3, 11);
Print(a);
return 0;
}
我们这里声明的是友元函数,外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数,所以友元函数可以访问私有成员,但是仍要说明是哪个对象。
2:友元函数可以在类定义的任何地方声明,不受类访问限定符限制
但是,一般为了使代码可视化,都会选择在最开始进行友元声明
3:一个函数可以是多个类的友元函数
4:友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
5:友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
6:友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元
下面来看代码示例:
class 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)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
这里一个函数是两个类的友元函数。这样两个类中的私有成员函数就都能够访问到了。
class A
{
// 友元声明
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)//友元记得传递对应对象的参数,有自己类的实例化对象的this指针的
{
}
void func2(const A& aa)//友元函数记得传对应的参数,自有己类的实例化对象的this指针的
{
}
private:
int _b1 = 3;
int _b2 = 4;
};
注意:只要是类的非静态成员函数,都是具有this指针的。
这里是一个友元类,此时B中的成员函数能够去访问A中的私有成员变量。
友元函数的缺点:
友元函数会破坏原本已经封装好的成员函数和成员变量,在项目中不建议多用。
内部类
定义:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
写个代码来加深定义理解:
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;
}
int _b1;
};
};
int main()
{
cout << sizeof(A) << endl;
return 0;
}
来看结果:
说明这里类A中既没有静态变量_k,也没有内部类B,类A中只有成员变量_h的4个字节。
内部类默认是外部类的友元类。
只要定义在内部类的类,就已经算是外部类的友元类,此时也就没有必要声明了,上述例子中类B可以使用类A的成员变量。
3:内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
这样也能够实现类的封装。
匿名对象
1:用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
2:匿名对象生命周期和临时对象的生命周期只在当前一行,⼀般临时定义一个对象当前用⼀下即可,就可以定义匿名对象
格式:类型(实参),中间不用写对象名
匿名对象最大的好处就是可以不用去写名字,一定程度上提升了程序的运行效率,来看示例代码:
int main()
{
A(1);
return 0;
}
A(1)整体叫做匿名对象。
再来看一个复杂的:
class Solution {
public:
int Sum_Solution(int n) {
return 0;
}
};
int main()
{
Solution().Sum_Solution(10);
return 0;
}
Solution()整体是匿名对象,我们再来调用对象中的成员函数并传参
结语
感谢大家阅读我的博客,不足之处欢迎在评论区与我交流,感谢大家支持!
人生如逆旅,我亦是行人!!加油!!