对类和对象的详细解析
目录
1.类的构成
public修饰的成员在类外可以直接被访问
private和protected修饰的成员不能在类外被访问
如果变量没有被这三个修饰词来修饰的话,则默认为private
2.this指针
关于this指针的一个经典回答:
有一个房子类
当你进入这个房子的时候,你可以看见桌子、椅子等,但是房子的全貌你看不见了
对于一个类的实例来说,你可以看见他的成员函数、成员变量
但是实例本身呢?
this这个指针就时时刻刻指向这个实例本身
this指针是指向对象的指针,存放对象的首地址
#include <iostream> using namespace std; class Student { public: void Setage(int age) { this->age = age;//this->age表示这个类所指向的age,单独的age是形参 } void Showage() { cout << age << endl; } private: int age; }; int main() { Student s; s.Setage(18); s.Showage(); return 0; } //输出结果为18
#include <iostream> using namespace std; class Student { public: void Setage(int age) { age = age;//age=age,并不会改变类里面age的值 } void Showage() { cout << age << endl; } private: int age; }; int main() { Student s; s.Setage(18); s.Showage(); return 0; } //输出结果是0
this指针是编译器帮我们加上的,不占用空间
#include <iostream> using namespace std; class Student { private: int age; }; int main() { cout << sizeof(Student) << endl;//结果为4 return 0; }
既然this指针指向类,那么类的地址和this的地址是一样的
#include <iostream> using namespace std; class Student { public: void test() { cout << this << endl; } private: int age; }; int main() { Student s; s.test(); cout << &s << endl; return 0; }
输出的地址是一样的
3.C++类里面的静态成员
静态成员是属于整个类的而不是某个对象,静态成员只存储一份供所有对象使用
3.1 类里面的静态成员函数
静态成员函数没有隐藏的this指针
#include <iostream> using namespace std; class Student { public: void Set(int age) { this->age = age; } void Showage() { cout << age << endl; } static void test() { cout<<this->age<<endl;//这个地方的age会报错,如果不加this也会报错 //静态成员函数不能引用非静态成员,但是可以在类的非静态成员函数里面使用静态成员 } private: int age; }; int main() { Student s; return 0; }
这就说明了静态函数是在这个类创建出来之前就出现了,也就是说静态函数的创建时间比这个类早,所以在这个静态函数里面使用类里面的东西会报错
那么应该如何使用这个函数呢
首先要注意一点:这个静态函数不能使用类里面的东西
class Student { static void test() { cout<<"i want to sleep"<<endl; } }; int main() { Student s; Student::test();//这里记住不能通过类名调用非静态成员函数 s.test(); //上面两句说明类的对象可以使用静态和非静态成员函数 return 0; }
这两种方法都可以调用这个静态函数
3.2 类里面的静态成员变量
静态成员变量在类的所有实例之间共享,并且可以在不创建类的情况下访问(在类创建之前就创建好了)
3.3静态成员变量的特点
共享性
静态成员变量在类所有的实例之间共享,这个类的每个对象的静态变量都是相同的
存储位置
静态成员变量存储在静态存储区,而不是每个对象的堆栈中。这使得他们可以在不创建类的实例的情况下访问
生命周期
与程序的生命周期一样。在程序启动的时候就创建了,在程序结束的时候就销毁
访问权限
静态成员变量可以通过类名来访问,也可以通过对象来访问。但是建议前者,这样子能强调他们的共享性
如何初始化?
class Student { private: static int a; }; int Student::a=0;
4构造函数
构造函数的主要作用不是开辟空间创建对象,而是为了初始化对象
特征
他不是开空间创建对象,而是初始化对象
1. 无返回值(也不用写void)
2. 函数名与类名相同
3. 构造函数可以重载
#include <iostream> using namespace std; class Student { public: Student(int a,int b,int c)//构造函数 { _a=a; _b=b; _c=c; } Student(int a,int b) { _a=a; _b=b; } void Show() { printf("%d %d %d",_a,_b,_c); } private: int _a,_b,_c; }; int main() { Student s(1,2,3);//创建s对象的同时调用构造函数 Student p(1,2);//函数重载 s.Show(); return 0; } //输出结果1 2 3
#include <iostream> using namespace std; class Student { public: void Show() { printf("%d %d %d",_a,_b,_c); } private: int _a,_b,_c; }; int main() { Student s; s.Show(); return 0; } /* 输出结果32767 0 0 如果类里面没有构造函数,则C++编译器会自动调用一个无参的默认构造函数 */
private: int _a=1,_b=1,_c=1; /* 这里对_a,_b,_c不是赋值不是初始化,而是给缺省值 原因:这里的变量都是声明,还没有创建出来 */
5析构函数
作用与构造函数相反。在对象销毁时自动调用,完成对象中资源的清理工作
特征
析构函数名是在类名的签名加~
无参数无返回值
一个类只能有一个析构函数,如果自己没有定义,系统会调用默认的析构函数。析构函数不能重载
对象生命周期结束时调用
析构函数的执行在return之前
#include <iostream> using namespace std; class S { public: S() { cout << "构造" << endl; } ~S() { cout << "析构" << endl; } }; int main() { S s; return 0; }
先输出构造,然后再输出析构
作用
当我们在类中声明了一些指针变量的时候,我们一般在析构函数里面释放这些指针变量所占有的空间,因为系统不会释放指针变量指向的空间,我们需要自己来delete
6.构造函数和析构函数的一些应用
6.1 定义为局部变量
先补充一个知识点:局部变量存储在栈区,全局变量和静态变量存储在静态区
#include <iostream> using namespace std; class Date { public: Date(int a) { _a = a; cout << "Date()->" << _a << endl; } ~Date() { cout << "~Date()->" << _a << endl; } private: int _a; }; int main() { Date a(1); Date b(2); return 0; }
输出结果
析构函数的时候为什么先输出~Date()->2呢?
首先我们要知道a和b变量都是局部变量,都存储在栈区,栈区遵循后进先出的原则
6.2 局部变量和全局变量同时存在时
#include <iostream> using namespace std; class Date { public: Date(int a) { _a = a; cout << "Date()->" << _a << endl; } ~Date() { cout << "~Date()->" << _a << endl; } private: int _a; }; Date b(2); int main() { Date a(1); return 0; }
输出结果
全局变量先定义,在程序结束之后再销毁(在局部变量销毁之后再销毁)
6.3 静态变量、局部变量、全局变量共存
#include <iostream> using namespace std; class Date { public: Date(int a) { _a = a; cout << "Date()->" << _a << endl; } ~Date() { cout << "~Date()->" << _a << endl; } private: int _a; }; Date b(2); int main() { Date a(1); static Date c(3); return 0; }
输出结果
创建变量按顺序来。首先是局部变量被销毁,静态区的变量(只考虑这个区域时)在销毁时也是类似“后进先出”一样来销毁
6.4 静态变量在循环中
#include <iostream> using namespace std; class Date { public: Date(int a) { _a = a; cout << "Date()->" << _a << endl; } ~Date() { cout << "~Date()->" << _a << endl; } private: int _a; }; int main() { for (int i = 0; i <= 1; i++) { static Date a(1); Date b(2); } return 0; }
输出结果
静态变量只会被创建一次,所以只有一次构造函数和一次析构函数
7.拷贝构造函数
浅拷贝
拷贝成员变量的值
#include <iostream> using namespace std; class Student { public: Student(int a, int b, int c) { _a = a; _b = b; _c = c; } /* 当有Student b(a)这行代码的时候 问题1:为什么要写成 Student &s呢? 写成Student &s表示s是a的别名,不需要调用拷贝构造函数 如果写成Student s的话,会无限递归 原因:在调用拷贝构造函数的时候, Student(Student s) { this->_a = s._a; this->_b = s._b; this->_c = s._c; } 把a传给s也会调用拷贝够咱函数,也就是说,调用一个拷贝构造函数会使得其内部嵌套另一个拷贝构造函数 那这个嵌套的拷贝构造函数也会继续嵌套一个,依次无限嵌套下去而导致死循环;所以要加一个& 问题2:那为什么加一个const呢? 因为我们要a的值不变,将其内部的值赋给b(s),所以我们最好加一个const来修饰来让a的值不变 只要a里面的值变,就会报错 因为有可能出现以下情况 Student(const Student &s) { a._a = this->_a; a._b = this->_b; a._c = this->_c; } 如果没有const,出现上述情况编译器是不会报错的 */ Student(const Student &s) { this->_a = s._a; this->_b = s._b; this->_c = s._c; } void Show() { printf("%d %d %dn", _a, _b, _c); } private: int _a, _b, _c; }; int main() { Student a(10, 20, 30); Student b(a); b.Show(); return 0; }
输出结果
8.重载
关键字:operator
8.1 运算符重载
#include <iostream> using namespace std; class Student { public: Student(int a, int b, int c) { _a = a; _b = b; _c = c; } bool operator==(const Student &b)//重载==运算符 { return _a == b._a && _b == b._b && _c == b._c;//a和b相等的条件 } private: int _a, _b, _c; }; int main() { Student a(10, 20, 30); Student b(10, 20, 30); cout << (a == b) << endl;//这里判断a和b是否相等只需要判断a和b里面的_a,_b,_c是否分别相等 return 0; }
#include <iostream> using namespace std; class Student { public: Student(int a, int b, int c) { _a = a; _b = b; _c = c; } int operator-(const Student &b) { return b._c - _c; } private: int _a, _b, _c; }; int main() { Student a(10, 20, 30); Student b(10, 20, 50); cout << (b - a) << endl; return 0; } //输出结果是a._c - b._c 结果为-20
这里就有一个问题
void operator++是怎么区别前置++和后置++的呢?
int operator++();//前置++ int operator++(int);//后置++ //规定:()里面无参数的为前置++,否则为后置++
注意点
#include <iostream> using namespace std; class A { public: A(int val){ _val=val; } void operator+(const A& d){ _val+=d._val; } void operator=(const A& d){ _val=d._val; } void Print(){ cout<<_val<<endl; } private: int _val; }; int main() { A a(3),b(4),c(7); c=a=b; /* c=a=b会报错,但是a=b,不会报错 因为c=a=b这句话首先执行的是a=b,然后返回值是void 所以 void operator=(const A& d){ _val=d._val; } 应该改成 A operator=(const A& d){ _val=d._val; return *this;//这里返回值是*this的一个拷贝(拷贝的话可能调用其中的拷贝函数或者构造函数什么的) } 所以最好写成 A& operator=(const A& d){ _val=d._val; return *this;//这里返回的是*this的别名,不会再去调用拷贝函数 } */ a.Print(); return 0; }
8.2 流重载
#include <iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void operator<<(ostream &out) // out是cout的别名 { out << _year << '/' << _month << '/' << _day << endl; } void operator>>(istream &in) { in>>_year>>_month>>_day; } private: int _year, _month, _day; }; int main() { Date a(2023, 9, 16); a>>cin;//a是被操作的对象,cin传给in a<<cout; return 0; }
const的参与
#include <iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { printf("%d %d %dn", _year, _month, _day); } private: int _year, _month, _day; }; int main() { Date a(2023, 9, 17); const Date b(1,1,1); a.Print(); b.Print();//这里会报错 /* 因为b是一个const Date类型的 b.Print()的原型是b.Print(&b),&b是一个const Date*类型的 void Print()的原型是void Print(Date* const this) &b从const Date*变成Date*权限放大了 所以我们将void Print()写成void Print() const,这样他的原型变成了: void Print(const Date* const this)是const Date*类型,后面那个const是修饰this 让this一直指向这个类且不能变 */ return 0; }
#include <iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print()const { printf("%d %d %dn", _year, _month, _day); } bool operator<(const Date& d) { return _year<d._year; } private: int _year, _month, _day; }; int main() { Date a(2023, 9, 17); const Date b(1,1,1); a<b;//不会报错 b<a;//这里会报错 /* bool operator<(const Date& d)的原型是 bool operator<Date* const this,const Date& d) 所以写成b<a的时候(b是const Date*类型) b由const Date*变成Date*,权限变大 可以写成 bool operator<(const Date& d)const */ return 0; }
9.初始化列表
初始化列表可以认为是成员变量定义的地方
(private里面是声明)
9.1格式
#include <iostream>
using namespace std;
class Date
{
public:
//格式如下
Date(int a)
: _a(a),
_b(a)
{
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
int &_b;
};
int main()
{
Date a(6);
a.Print();
return 0;
}
9.2例子
每个成员变量在初始化列表中只能出现一次
类中包含以下成员,必须放在初始化列表位置进行初始化
1.引用成员变量
2.const成员变量
3.自定义类型成员
class A { public: A(int a) : _a(a) { } void Print() { cout << _a << endl; } private: int _a; }; class B { public: B(int a, int b) : _b(b), _a(a), _n(10), _r(b) { } void Print() { _a.Print(); cout << _b << endl; } private: int _b; A _a; const int _n; int& _r; };
成员变量在类中声明的次序就是其在初始化列表里面初始化的次序,与其在初始话列表里面的先后顺序无关
例子:
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
:_a(a),
_b(_a)
{}
void Print()
{
cout<<_a<<endl;
cout<<_b<<endl;
}
private:
int _b;
int _a;
};
int main()
{
A a(3);
a.Print();
return 0;
}
/*
输出结果是 3 随机值
因为先是把_a赋值给_b,再把a赋值给_a
*/
9.3explicit关键字
用explicit修饰的构造函数,会禁止单参构造函数的隐式转换
首先搞明白什么是隐式转换
class A { public: A(int a)//单参构造函数 : _a(a) { } private: int _a; }; //定义一个类对象 A a(10); /* 此时_a的值为10,如果进行以下操作,a=20,这样子会把_a的值变为20 编译器会用20(将20赋值给无名对象的_a)构造一个无名对象 最后用无名对象赋值给对象a,这种转换就叫隐式转换 */
隐式转换其实还有很多,比如
int a=2;
double b=a;
编译器会先引入一个中间变量,类型为double,然后再将这个值赋值给b
#include <iostream> using namespace std; class A { public: A(int a) /* 如果这里改成explicit A(int a)的话 a=5那行代码会报错 因为explicit会禁止单参构造函数的隐式转换 */ : _a(a) { } void Print() { cout << _a << endl; } private: int _a; }; int main() { A a(3); a = 5; a.Print(); return 0; }
9.4 匿名对象
生命周期只有它所在的那一行
class A { public: A(int a) :_a=a {} private: int _a; }; A a(10); a = A(20); a.Print(); 如果不使用匿名对象,那就需要: A a(10); A b(20); a=b; 这样子来操作 /* A(20)表示创建一个匿名对象,他的_a值为20 这个匿名对象创建的时候也会调用构造函数和析构函数 */
10.友元类
如果类A是类B的友元类,那么B可以调用A里面的private成员和protected成员
但是这样子做会破坏类的封装性
注意点
1. 友元关系不能被继承
2. 友元关系是单向的,A是B的友元,但是B不一定是A的友元
3. 友元不具有传递性,比如A是B的友元,B是C的友元,但是A不是C的友元
#include <iostream> using namespace std; class A { public: friend class B;//代表A是B的友元类,B可以访问A里面的所有成员 A(int a) { _a=a; } private: int _a; }; class B { public: B(int b) { _b=b; } void Show_A_a(A a) { cout<<a._a<<endl;//这里就体现了可以访问A定义的对象a里面的私有成员 } private: int _b; }; int main() { A a(666); B b(555); b.Show_A_a(a); return 0; }
10. 2内部类
定义:如果一个类定义在另一个类的里面,那这个里面的类就叫做内部类
内部类是外部内的友元类,但是外部类不是内部类的友元类
比如A是外部类,B是外部类
那么定义一个类的时候应该A::B b;定义一个对象b,b能调用A和B里面的成员,但是如果A a,那a不能调用B里面的成员