类和对象
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的定义
下面我们自己尝试定义一个类:
int main()
{
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
Data d1;
d1.Init(2024, 3, 30);
d1.Print();
return 0;
}
运行结果:
这里我们简单的定义了一个日期类, 这里讲几个细节:
在定义类的时候如果不说明是共有(public)还是私有(private)的时候,默认为私有,私有的情况下外面是无法访问到这里的数据的,这个我们等会就会说到,比如:
int main()
{
class Data
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
Data d1;
d1.Init(2024, 3, 30);
d1.Print();
return 0;
}
这里的Class类部分没有说明共有还是私有,这样编译的时候就会出现错误,因为外部无法访问到里面的数据:
定义规范
int main()
{
class Data
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
void Print()
{
cout << year << "/" << month << "/" << day << endl;
}
int year;
int month;
int day;
};
Data d1;
d1.Init(2024, 3, 30);
d1.Print();
return 0;
}
看到上面这个程序,是不是感到很混乱,赋值和输出的到底是哪个?因此一般情况下我们在定义类的时候会在成员变量前面加一个_,也就是第一中定义的方法,用来区分成员变量和函数参数。
那么展开之后就只在类中访问吗?
不是的,只是先在类中访问,访问不到的时候会在局部域和全局域中国搜索。
类的访问限定符及封装
访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符说明:
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
当声明和定义分离时,想要访问到类中的函数就要在定义部分加入些许修饰,也就是上述的Data::。
为什么编译器会访问不到这里的类中的函数声明和变量呢?
我们知道,编译器的搜索原则是从当前域,全局域这样一个顺序来进行搜索的,而我们定义的类也是属于一个命名空间域,我们想要取访问该域中的数据就需要展开或者指定去访问。
类的实例化
用类类型创建对象的过程,称为类的实例化。
举例:
class Data
{
public:
void Init(int year, int month, int day);
void Print();
int _year;
int _month;
int _day;
};
实例化的一个重要标志就是有没有开空间,就像上述的代码,只是描述了一个类的模子,并没有具体的空间来存储。
再看下面:
class Data
{
public:
void Init(int year, int month, int day);
void Print();
int _year;//声明,并没有给出定义
int _month;
int _day;
};
int main()
{
Data d1;//实例化
d1.Init(2024, 3, 30);
d1.Print();
return 0;
}
这个代码就在主函数里对类Data进行实例化,产生了d1,这就是类的实例化。
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
类对象模型
如何计算类对象的大小
class Data
{
public:
void Init(int year, int month, int day);
void Print();
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
cout << sizeof(d1) << endl;
return 0;
}
上述计算结果是多少?
这里就直接给出答案:12
如果该类对象的大小是12,通过我们结构体部分的知识,这个类对象中的3个成员变量的大小就已经是12,那类对象成员函数的大小去哪里了?成员函数不是包含在来中的吗?
下面就来讲一下类中成员变量和成员函数的内存分布:
在类对象实例化的时候,成员变量都是存在不同的单独空间中,而类对象成员函数是存在公共代码区,大家公用这块空间,在计算类大小的时候是不被计入的。
计算下面类对象的大小:
//类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
按照常规思维,上述类中连个成员变量都没有,大小当然是0啊,当然凡是都有特殊情况,这里的结果是0。
为什么会是0呢?
解答:没有成员变量的类对象大小是1byte,占位,标识对象实例化时,定义出来存在过。
当类对象中没有成员变量,只有成员函数,这个时候难道类对象实例化的时候就不开空间储存了吗?这显然是不合理的,因此就算没有成员变量也会有1byte大小的空间。
下面上一道硬菜,可以引出我们下一个知识点:
class A2 {
public:
void f2()
{
cout << "void f2()" << endl;
}
};
int main()
{
A2 a2;
A2* p1 = &a2;
p1->f2();
A2* p2 = nullptr;
p2->f2();
return 0;
}
这里可能会出现那些错误?
编译错误
运行错误
正常运行
正常情况下,语句p2->f2,对空指针进行解引用必然会出现错误,这很显然。
但是这里必然不会这么简单:结果是正常运行。
我们知道,类对象在进行实例化的时候,成员函数是被储存在一个公共代码区的,因此,这里的代码并不存在对空指针的解引用,程序正常运行。
这里的原因也和我们接下来要将的this指针有关,下面我们来讲一下this指针:
this指针
先看一下这段代码:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
可以看到,我们在进行成员函数调用的时候,并没有说明传递的是哪一个类对象的成员变量,为什么编译器就可以准确的打印出我们想要的参数?
解答:因为编译器在进行传递参数的时候,会隐式传递一个指针,就是这个指针让编译器准确的找到我们想要的类对象成员变量,这个就是this指针!
我们看下面这段代码:
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print(Data* const this)
{
cout <<this->_year<< "-" <<this->_month << "-"<<this->_day <<endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print(&d1);
d2.Print(&d2);
return 0;
}
this指针的特性
- this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
这里const修饰的是指针本身,即可以对指针对象进行解引用修改值,但是不能改变指针指向。- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
关于this指针,有两个常考的面试题:
1.this指针存在哪里?
A、堆 B、栈 C、静态区 D、常量区 E、对象里面
解答:首先,this指针不在对象中,因为我们在计算类对象大小的时候,只计算了类成员变量的大小,而成员函数是单独存在公共代码区的,而这其中并没有提到this指针,因此this指针并不存在对象当中;
由malloc开辟出的空间是在堆上开辟的,排除A;
我们知道,由static修饰和全局变量才是存在静态区的,排除C;
下面说一下常量区,看一下一下这段代码:
const int i = 0;
int j = 1;
const char* p = "xxxxxxx";
以上代码中,变量i、j、p都是存在栈上的,而指针p指向的字符串"xxxxxxx"是存在常量区的;
因此这里的答案就是:this指针式存在栈上的;
原因:this指针式形参,this指针这一节给出的第一个代码来看,this指针接收的是从主函数传递过来的实参,因此this指针式形参,储存在栈上。
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
在看下面这个代码:
class A
{
public:
void PrintA()
{
cout<< "void Print()" <<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
(*p).PrintA();
return 0;
}