🔥 本文专栏:c++
🌸作者主页:努力努力再努力wz
💪 今日博客励志语录 :
你的人生剧本,不是父母的续集,不是子女的前传,更不是朋友的外传------你是自己故事的主角
★★★ 本文前置知识:
类和对象(中)
类和对象(上)
看完本文,你会学到:
1.知道什么是初始化列表以及如何使用初始化列表及其注意的相关细节
2.知道什么是静态成员变量以及如何使用静态成员变量及其注意的相关细节
3.知道什么是友元函数以及如何使用友元函数及其注意的相关细节
4.知道什么是匿名对象以及如何使用匿名对象及其注意的相关细节
那么你准备好在类和对象的知识海洋中遨游了吗?
初始化列表
1.什么是初始化列表
那么我们之前我们知道刚实例化创建好的对象完成自身非静态的成员变量的初始化的工作是交给构造函数来完成,而其中对于构造函数来说,其完成对于成员变量的初始化则是交给初始化列表来完成的,所以这里我们就得完善一下构造函数的构成了,那么构造函数是由两部分所构成,分别是初始化列表以及构造函数体这两部分所构成,那么首先在具体讲解初始化列表怎么使用之前,我们先来见一见初始化列表的样子,那么以日期类为例:
cpp
class date
{
private:
int _year;
int _month;
int _day;
public:
date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{
}
};
那么date构造函数声明下面紧跟着的就是初始化列表,那么初始化列表的定义就是第一个成员变量前面加上冒号然后括号后面就是该成员变量初始化的值,然后后面的成员变量依次用逗号来隔开
2.初始化列表怎么使用
那么没讲初始化列表之前,那么我们对于成员变量的所谓的初始化操作都是定义在构造函数体中,那么现在有了初始化列表的概念之后,那么我们如果自己定义的构造函数没有显示的定义初始化列表,那么同样编译器会隐式的为该构造函数定义一个初始化列表,并且编译器执行构造函数的顺序是先执行初始化列表再执行构造函数体内的内容,所以成员变量在执行构造函数体的内容之前其实已经完成了初始化了,但是只不过编译器提供的默认的构造函数的初始化列表对于内置类型它不会做任何处理,而对于自定义类型则会调用其默认的构造函数
cpp
class date
{
private:
int _year;
int _month;
int _day;
public:
//编译器默认生成的构造函数
date()
:_year()
,_month()
,_day()
{
}
};
所以我们之前在构造函数体内所定义的访问成员变量的各种行为,其实是对成员变量进行赋值或者说修改而不是对成员变量进行初始化,那么有的小伙伴可能就会有疑问,那么既然按照你这么说,那么初始化列表已经完成了构造函数的初始化工作,那么还要构造函数体干嘛,那么这个构造函数体岂不是显得冗余?
实则不然,那么初始化列表的工作可以概括为给对象中的各个非静态的成员变量赋一个初始值,但是初始化列表只能给成员变量设置初始值,但是它却无法检查初始值是否有效,所以此时就需要构造函数体来完成这个工作,比如检查成员变量的初始值是否有效,那么我们以栈为例:
那么假设我们通过动态数组的方式来实现栈,那么栈对应的类中的成员变量就得包括一个指针,指向在堆上开辟申请的一个动态数组的首元素的地址以及一个追踪栈顶的变量top和记录目前栈的最大容量的变量maxsize,那么我们可以在初始化列表中调用malloc函数,其会在堆上申请一片连续的空间然后返回首元素的地址,但是malloc函数可能会出现开辟失败的情况,那么此时它会返回一个空指针,所以这时就需要我们在构造函数体内定义一个检查的逻辑,避免后序通过对象来访问该空指针
cpp
class stack
{
private:
int* ptr;
int top;
int maxsize;
public:
stack(int default)
:ptr((int*)malloc(default*sizeof(int)))
,top(-1)
,maxsize(default)
{
if(ptr==nullptr)
{
perror("malloc fail");
return;
}
}
};
所以初始化列表是来完成对于成员变量的初始化,而构造函数体可以用来定义对于初始值的检查等一系列逻辑
其次如果我们的类如果中有自定义类型的成员变量,并且该自定义类型的成员变量对应的类中没有提供无参数或者全缺省的构造函数,那么我们就得在初始化列表中为该自定义类型的带参的构造函数提供参数
cpp
#include<iostream>
using namespace std;
class student //student类没有无参数以及全缺省的构造函数
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
int getage()
{
return _age;
}
};
class person
{
private:
int _id;
student a;
public:
person(int id=10001)
:_id(id)
{
}
};
int main()
{
person s1;
return 0;
}

所以我们得在初始化列表中显示调用自定义类型的带参数的构造函数:
cpp
#include<iostream>
using namespace std;
class student
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
int getage()
{
return _age;
}
};
class person
{
private:
int _id;
student a;
public:
person(int id=10001)
:_id(id)
,a("WangZ",20)
{
}
student& getstu()
{
return a;
}
};
int main()
{
person s1;
cout << s1.getstu().getage() << endl;
return 0;
}

3.初始化列表的相关细节补充
我们知道编译器执行构造函数,那么会首先执行初始化列表的内容然后再执行构造函数体里面的内容,那么其中对于初始化列表的执行顺序则是按照你类中成员变量的声明的顺序来执行而不是按照你初始化列表的顺序来执行,那么比如按照上文的stack类,由于maxsize代表着数组的最大长度,那么有些小伙伴可能在malloc的时候,那么他直接就是用maxsize的值来作为在堆上申请开辟的数组长度,但是根据上文的stack类的成员变量声明的顺序,那么maxsize的声明是在指针ptr之后,那么意味着ptr会先进行初始化然后再执行maxsize的初始化,那么此时maxsize的值就是随机值,所以得注意初始化列表的执行顺序。
那么不建议在初始化列表中用其中一个成员变量的值来初始化另一个成员变量
cpp
class stack
{
private:
int* ptr;
int top;
int maxsize;
public:
stack(int defaultsize)
:ptr((int*)malloc(maxsize*sizeof(int)))
,top(-1)
,maxsize(defaultsize)
{
if(ptr==nullptr)
{
perror("malloc fail");
return;
}
}
};
其次就是关于成员变量的类型是引用的时候,由于引用在声明的时候就得有定义,那么意味着类中有引用的话,那么我们就得显示在初始化列表中对引用进行定义
其中在用初始化列表来初始化引用的时候就得注意,因为初始化列表是构造函数的一部分,那么通过初始化列来初始化引用的时候,那么必然会调用构造函数,而调用函数会为该函数在栈上开辟一份空间,那么而初始化列表就是将形参的值给赋值拷贝给成员变量,那么引用在构造函数初始化的时候就会绑定到给形参上,那么一旦构造函数调用结束,那么形参随着函数栈帧一起被销毁,那么此时引用却依然指向那片被销毁的空间,那么我们通过对象来访问该引用指向的内容必然是随机值,也就会出现野引用问题
所以为了避免这种情况发生,那么我们的形参就得设置为引用,那么这样引用就能够指向到外部的空间而不是函数内的局部变量
cpp
class person
{
private:
char* _name;
int _age;
int& _heigeht;
public:
person(char* name,int age,int& height)
:_name(name)
,_age(age)
,__height(height)
{
}
}
最后就是对于const属性的成员变量,那么也得在初始化列表显示的初始化,因为const修饰的变量必须在声明的时候就进行初始化
静态成员变量
为什么会有静态成员变量
那么我们可以通过类来描述现实生活中的一个个实体,比如我们要描述一个学生,那么我们可以定义一个student类,那么这些按照student类实例化出的对象可能具有共同的属性,比如他们都来自同一个学校,那么我们就没必要为每一个实例化的对象都分配一个空间给该成员变量来记录这个属性,而是将这个共同的属性给提取出来,作为类共享,可以减小对象的空间,那么这就是静态成员变量所应用的场景
静态成员变量怎么用
那么我们在类中定义的成员变量默认的属性都是非静态的,那么要在类中声明一个静态成员变量,则需要static关键字:
cpp
class student
{
private:
char* name;
int age;
int height
static int classnum;//声明静态成员变量
}
静态成员变量的相关使用细节
那么一旦成员变量被定义成静态,那么它便不是属于某个实例化的对象,而是整个类或者说按照这个类实例化出来的所有对象所共享的,而我们知道当对象被创建出来的那一刻,会调用构造函数来完成对象中非静态的成员变量的初始化,所以构造函数是来完成非静态成员变量的初始化的,构造函数又分为初始化列表以及构造函数体两部分,那么静态成员变量一定无法通过初始化列表来完成初始化,那是给非静态的成员变量来初始化,假设允许的话,那么每次创建一个对象,便会调用一次构造函数,那么意味着就会覆盖一次静态成员变量的内容,所以通过初始化列表来初始化初始化静态成员变量是不现实的,由于无法用初始化列表,那么自然我们也无法在类中声明静态成员变量的时候提供缺省值:
cpp
#include<iostream>
using namespace std;
class student
{
public:
char* _name;
int _age;
static int _classnum = 10;
};
int main()
{
student a;
return 0;
}

所以我们只能在类外部定义静态成员变量:
cpp
class student
{
private:
char* _name;
int _age;
static int _classnum;
}
student::classnum=10;
而对于static修饰的变量,那么它的空间是在静态区上开辟,并且在程序开始的时候便会为static修饰的变量分配空间,所以其生命周期和程序的生命周期一样长,相比于非静态的成员变量,非静态的成员变量只有当对象被创建的时候,在对象的内部分配空间,所以也就意味着我们访问静态的成员变量可以直接指定类域去访问
cpp
#include<iostream>
using namespace std;
class student
{
public:
const char* _name;
int _age;
static int _classnum;
student(const char* name,int age)
:_name(name)
,_age(age)
{
}
int getage()
{
return _age;
}
};
int student::_classnum=10;
int main()
{
cout<<student::_classnum<<endl;
return 0;
}

其次我们也可以通过对象去访问静态成员变量,
cpp
#include<iostream>
using namespace std;
class student
{
public:
const char* _name;
int _age;
static int _classnum;
student(const char* name,int age)
:_name(name)
,_age(age)
{
}
int getage()
{
return _age;
}
};
int student::_classnum=10;
int main()
{
student a("WangZ",20);
cout<<a._classnum<<endl;
return 0;
}

静态成员变量也会被访问限定符所修饰,所以一旦静态变量是私有属性,那么我们就无法通过对象以及在类外面指定类域来访问,那么此时我们有两种方式可以访问私有属性的静态成员变量,第一种方式则是通过成员函数,因为成员函数能够访问类的所有包括静态以及非静态的成员变量:
cpp
#include<iostream>
using namespace std;
class student
{
private:
const char* _name;
int _age;
static int _classnum;
public:
student(const char* name,int age)
:_name(name)
,_age(age)
{
}
int getage()
{
return _age;
}
int getstatic_val()
{
return _classnum;
}
};
int student::_classnum=15;
int main()
{
student a("WangZ",20);
cout<<a.getstatic_val()<<endl;
return 0;
}

第二中方式则是我们可以定义一个静态的成员函数,那么静态的成员函数和普通的成员函数不同的是,那么它是属于类共享,而不是某一个对象特有,那么意味着我们通过对象去调用该函数,那么编译器不会隐式的传递一个this指针
并且对于静态成员函数来说,那么非静态成员函数可以调用静态的成员函数,而对于静态的成员函数来说,那么它要调用非静态的成员函数,那么我们就得在静态函数体内部去定义一个对象,然后通过对象来调用非静态的成员函数:
cpp
#include<iostream>
using namespace std;
class student
{
private:
const char* _name;
int _age;
static int _classnum;
public:
student(const char* name,int age)
:_name(name)
,_age(age)
{
}
int getage()
{
return _age;
}
static int getstatic_val()
{
student a("WangZ",19);
int age=a.getage();
return age;
}
};
int student::_classnum=15;
int main()
{
cout<<student::getstatic_val()<<endl;
return 0;
}

最后我们再来探讨静态的成员函数与j静态的普通函数的区别,其中静态的成员函数可以做到声明和定义分离,但是对于静态的普通函数来说,那么它无法做到声明和定义分离,也就是静态函数的声明保存在头文件中,静态函数的定义保存在源文件中,这是因为静态函数的属性是内部链接,而在编译阶段,编译器是以一个文件作为独立的编译单元进行编译,那么所谓内部链接,那么只能是该静态函数的声明和定义在一个文件中,而不能分离,然后它会记录进该局部的符号表中,并且被标记为内部链接,那么在链接阶段生成全局符号表会被忽略除去,所以一旦静态的普通函数只有声明而没有定义便会报错
友元函数
1.什么是友元函数
那么我们知道要访问类中的成员变量,我们目前有两种方式可以访问类中的成员变量,第一种方式就是实例化一个对象,然后通过该对象来访问成员变量,但是前提是成员变量是公有属性,其次则是通过成员函数去访问类中的所有的成员变量,那么还有第三种方式,便是通过友元函数来访问类中的所有成员变量,那么友元函数其实本质上就是一个普通的函数,但是它和普通的函数的区别就是,一般的普通函数无法访问类中私有属性的成员变量,但是其对于友元函数则开了一个后门,允许友元函数访问其类中私有属性的成员变量,但是前提是你得在类中声明该普通函数是该类的友元函数
2.友元函数怎么使用
那么我们想让外部的普通函数作为该类的友元函数,那么就得在类声明该友元函数,那么就得使用friend关键字:
cpp
#include<iostream>
using namespace std;
class student
{
private:
friend void hellostudent(student& d1); //声明友元函数
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
const char* getname()
{
return _name;
}
};
void hellostudent(student& d1)
{
cout << "hello " << d1.getname() << endl;
}
int main()
{
student s1("WangZ", 20);
hellostudent(s1);
return 0;
}

3.友元函数的相关使用细节
那么友元函数其实就是一个普通函数,那么我们如果要通过调用友元函数来访问成员变量的话,那么调用友元函数不会调用成员函数那样,会隐式的传递一个this指针,所以我们在设置友元函数的参数的时候,一定要显示的设置一个对象的引用或者指针
其次友元函数在类中的声明不会受到访问限定符的修饰,不会存在所谓的私有属性的友元函数声明者一说,
并且我们友元函数还可以拓展到友元类上,那么被声明为友元类的类,那么可以访问该类的所有的成员变量,但是注意友元类是具有单向的,也就是人家允许你到他家来做客,但是你却没有邀请他到你家做客,所以友元类可以访问该类的所有成员变量,但是该类无法访问友元类的成员变量:
cpp
#include<iostream>
using namespace std;
class dog
{
friend class student; //声明友元类
private:
const char* _name;
public:
dog(const char* name)
:_name(name)
{
}
const char* getname()
{
return _name;
}
};
class student
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
const char* getname()
{
return _name;
}
void hellodog(dog& d1)
{
cout << "hello " << d1.getname() << endl;
}
};
int main()
{
student s1("WangZ", 20);
dog d1("wangcai");
s1.hellodog(d1);
return 0;
}

最后我们其他类的成员函数也可以申请为友元函数,但是声明的时候得注意级域作用限定符:
cpp
class A
{
friend void B::aceess();
};
class B
{
public:
void accsee()
};
匿名对象
1.什么是匿名对象
那么我们实例化创建出的对象一般都是带有名字的,所以我们可以在之后的代码中通过对象名来访问该对象中的成员变量或者成员函数,而匿名对象,那么顾名思义,就是我们在创建对象的时候,没有指定对象名,那么创建出来的该对象是没有对象名的
2.匿名对象怎么使用
知道了什么是匿名对象之后,那么如何创建匿名对象呢:
cpp
class A
{
.....
};
int main()
{
A();
}
那么这就是创建匿名对象的一个方式,对象的类型后面直接跟一个括号,而声明有名对象的时候,那么类型和括号之间的内容便是对象名,那么匿名对象就直接把对象名这一部分给剔除了,其中我们要注意的就是如果匿名对象对应的类中没有无参或者全缺省的构造函数,那么我们在创建匿名对象的时候,那么我们就得在括号里传递参数,因为既然是创建对象,那么肯定会调用构造函数,那么如果只有带参数的构造函数,那么就得我们创建的时候显示传递参数:
cpp
#include<iostream>
using namespace std;
class student
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
const char* getname()
{
return _name;
}
void hellostudent()
{
cout << "hello " << _name << endl;
}
};
int main()
{
student ();
return 0;
}

3.匿名对象的相关使用细节
那么知道了什么是匿名对象以及匿名对象如何创建,那么我们就得知道匿名对象的使用场景,那么由于匿名对象没有对象名,那么我们无法在之后的代码中访问到匿名对象的成员变量,所以匿名对象的生命周期就在创建它的那一刻,也就是说,一旦执行完创建匿名对象的代码,到下一行代码的时候,那么匿名对象就会被销毁了,所以匿名对象的第一个使用场景,便是调用成员函数,如果你不想为对象开辟空间,但是想调用成员函数,那么可以通过创建一个临时的匿名对象,然后通过匿名对象来调用成员函数:
cpp
#include<iostream>
using namespace std;
class student
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
const char* getname()
{
return _name;
}
void hellostudent()
{
cout << "hello " << _name << endl;
}
};
int main()
{
student ("WangZ",20).hellostudent();
return 0;
}

其次如果某个函数的参数涉及到对象的传值拷贝,那么你也可以创建一个临时的匿名对象,然后通过这个匿名对象拷贝赋值给形参,然后拷贝结束之后,匿名对象就会被销毁了:
cpp
#include<iostream>
using namespace std;
class dog
{
public:
const char* _name;
dog(const char* name)
:_name(name)
{
}
};
class student
{
private:
const char* _name;
int _age;
public:
student(const char* name, int age)
:_name(name)
, _age(age)
{
}
const char* getname()
{
return _name;
}
void hellostudent()
{
cout << "hello " << _name << endl;
}
void hellodog(const dog& d1)
{
cout << "hello " << d1._name << endl;
}
};
int main()
{
student s1("WangZ", 20);
s1.hellodog(dog("WangWang"));
return 0;
}

这里我们要注意的就是如果我们引用指向绑定的是一个匿名对象,那么匿名对象是具有常性的,那么我们只能通过常引用来指向该匿名对象,并且一旦匿名对象被常引用给指向,那么该匿名对象的生命周期就会被延长,那么和常引用的生命周期一样长
综合类和对象的知识点实现一个date类
最后我们要综合之前的类和对象的知识,来完成一个date类,其中要实现日期的加减天数以及日期的比较以及两个日期的相减等功能
date.h:
cpp
#pragma once
#include<iostream>
using namespace std;
class date
{
private:
int _year;
int _month;
int _day;
public:
date(int year, int month, int day);
int getyear() const;
int getmonth() const;
int getday()const;
date(const date& d1);
bool operator== (const date& d1);
bool operator> (const date& d1);
bool operator>=(const date& d1);
bool operator<(const date& d1);
date operator+(int days);
date& operator+=(int days);
date operator-(int days);
int operator-(const date& d1);
date operator++();
date& operator-=(int days);
int Getmonthday(int year, int month);
};
ostream& operator<<(ostream& out, const date& d1);
date.cpp:
cpp
#include"date.h"
date::date(int year = 2025, int month = 5, int day = 31)
:_year(year)
, _month(month)
, _day(day)
{
}
date::date(const date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
bool date::operator==(const date& d1)
{
return _year == d1._year && _month == d1._month && _day == d1._day;
}
bool date::operator>(const date& d1)
{
if (_year > d1._year)
{
return true;
}
else if (_year == d1._year)
{
if (_month > d1._month)
{
return true;
}
else if (_month == d1._month)
{
if (_day > d1._day)
{
return true;
}
}
}
return false;
}
bool date::operator>=(const date& d1)
{
return (*this) > d1 || (*this) == d1;
}
bool date::operator<(const date& d1)
{
return !((*this) >= d1);
}
int date::Getmonthday(int year, int month)
{
int arr[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return arr[month - 1];
}
}
date& date::operator+=(int days)
{
_day += days;
int curdays = Getmonthday(_year, _month);
while (_day > curdays)
{
_day -= curdays;
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
curdays = Getmonthday(_year, _month);
}
return *this;
}
date date::operator+(int days)
{
date temp = *this;
temp += days;
return temp;
}
date date::operator-(int days)
{
date temp = *this;
temp -= days;
return temp;
}
date& date::operator-=(int days)
{
if (days < 0)
{
return *this += (-days);
}
while (days > 0)
{
if (_day > days)
{
_day -= days;
break;
}
else
{
days -= _day;
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day = Getmonthday(_year, _month);
}
}
return *this;
}
date date:: operator++()
{
(*this) += 1;
return *this;
}
int date:: operator-(const date& d1)
{
int num = 0;
date max;
date min;
if (*this < d1)
{
max = d1;
min = *this;
}
else
{
max = *this;
min = d1;
}
while (!(min == max))
{
++min;
num++;
}
return num;
}
int date::getyear() const
{
return _year;
}
int date::getmonth() const
{
return _month;
}
int date::getday() const
{
return _day;
}
ostream& operator<<(ostream& out, const date& d1)
{
out << d1.getyear() << " " << d1.getmonth() << " " << d1.getday();
return out;
}
main.cpp:
cpp
#include"date.h"
int main()
{
date d0(2025, 5, 10);
date d1(2025, 5, 31);
if (d0 < d1)
{
cout << "d1 is max" << endl;
}
else
{
cout << "d0 is max" << endl;
}
cout << (d0 + 10) << endl;
cout << (d1 - 20) << endl;
return 0;
}

结语
那么这就是本期关于类和对象的全部介绍了,那么学习完了类和对象之后,那么恭喜耐心看到这里的读者,那么你顺利逾越了c++的一座大山,那么之后我会讲解内存管理以及模版,那么这两个内容学完之后,你会见识到c++的好玩之处,那么我会持续更新,希望你多多关注,如果本文有帮组到你,还请三连加关注哦,你的支持就是我创作的最大的动力!