1.类的引入
C++兼容了C语言结构体的用法,但是同时又升级成了类。结构体中只能定义变量,类中不仅可以定义变量,还可以定义函数。
例如,数据结构中实现栈,结构体stack中只定义了变量,要实现的函数在结构体外定义。以下代码就是对该实例的实现以及使用。
//C语言中结构体实现
struct stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct stack* s,int capacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == nullptr)
{
perror("malloc fail");
return;
}
s->a = tmp;
s->capacity = capacity;
s->top = 0;
}
void StackPush(struct stack* s, int x)
{
//扩容
s->a[s->top++] = x;
}
//C++中对结构体升级为类后实现
struct stack
{
//成员变量
int* a;
int top;
int capacity;
//成员函数
void StackInit(int icapacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * icapacity);
if (tmp == nullptr)
{
perror("malloc fail");
return;
}
a = tmp;
capacity = icapacity;
top = 0;
}
void StackPush(int x)
{
//扩容
a[top++] = x;
}
};
int main()
{
struct stack s;
StackInit(&s);
StackPush(&s, 1);
//类名可以代表类型
stack1 s1;
s1.StackInit();
s1.StackPush(1);
}
C++中更喜欢用 class 来定义类。
2.类的访问限定符及封装
2.1访问限定符
请看下面的代码,思考结果是什么。
class Date
{
int _year;
int _month;
int _day;
void Init(int year, int month, int day)
{
_year = year;;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main()
{
Date s;
s.Init(2003, 10, 1);
s.Print();
return 0;
}
是在屏幕上打印出 2003/10/1 吗?
不是的,反而会报错。显示函数 Init 和函数 Print 不可访问。这是为什么呢?
因为访问限定符的存在。
访问限定符也是C++实现封装的方式:用类将对象的属性与方法结合在一块,将对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符的说明】
①public修饰的成员在类外可以被直接访问;
②private和protected修饰的成员在类外不能直接被访问
③访问权限作用域从该访问限定符出现位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}即类结束。
④class的默认访问权限为private,struct的默认访问权限是public(因为struct要兼容C)
要想让刚才的代码能够正确运行,应该对成员的访问权限进行设定。虽然class的默认访问权限是private,不过为了更清晰便将其标注出来。一般对于成员变量,只允许类内访问,将其设置为private。
class Date
{
private:
int _year;
int _month;
int _day;
public:
void Init(int year, int month, int day)
{
_year = year;;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
2.2封装
面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
在C++中实现封装,可以通过类将数据及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接使用。
3.类的定义
类定义的格式:
class classname
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
类有两种定义方式:
①声明和定义全部放在类体中。成员函数如果在类中定义,编译器可能会当将其作内联函数处理。这种方式是比较简单的。
②成员函数声明与定义分离。
这样实现的结果是不对的,报错显示未声明的标识符。因为在未指定作用域的情况下,搜索原则是先在局部区域查找,没找到再从全局域查找。而上面形式中 Init函数作用域内没有声明_year等变量,全局域中也未声明,所以会出错。
正确形式应是在定义时在成员函数前加类名::
4.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
class Person
{
private:
char _name[20];
char _gender[3];
int _age;
public:
void PrintPersonInfo();
};
//指定函数PrintPersonInfo属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
5.类的实例化
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
一个类可以实例化出多个对象。
圈出的部分是变量的声明还是定义呢?
是声明。变量的定义和声明有一个区别,变量定义需要开辟空间。那么该部分应该怎样定义?
也不可以通过下面的方式对其进行访问,因为它只是对_yaer等的声明,通过类域Date::找到了_year的出处,但是并没有空间。只能通过定义的类对象去访问,如s._year++;
6.类对象模型
6.1如何计算类对象的大小
class Date
{
public:
int _year;
int _month;
int _day;
void Init(int year, int month, int day)
{
_year = year;;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main()
{
Date s;
cout << sizeof(s) << endl;
return 0;
}
结果是12,根据结果看好像是三个成员变量在内存中所占的字节数。可是类中既可以有成员变量,也可以有成员函数,那么一个类的对象中到底包含了什么?如何计算类的大小?
先看以下代码,看看两个_year是同一空间吗,两个Init函数地址一样吗?
int main()
{
Date s1;
Date s2;
s1._year = 2000;
s2._year = 2000;
s1.Init(2003, 10, 1);
s2.Init(2003, 10, 1);
return 0;
}
通过汇编语言可以看出,s1._year和s2._year占用了不同空间,而s1.Init和s2.Init是相同的地址。
6.2类对象存储方式的猜测
①对象中包含类的各个成员
这样设计是可以的,但是有缺陷。
缺陷:每个对象中的成员是不同的,但是调用的是同一份函数,按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同的代码保存多次,浪费空间。
②只保存成员变量,成员函数存放在公共的代码段
显然在以上两种存储方式中,计算机是按照第二种来存储的。
结论:
一个类的大小,实际就是该类中“成员变量”之和,当然要注意内存对齐;
当类中只有成员函数时,类大小也是1,为了占位,标识对象实例化时,定义出来存在过;
注意空类的大小,空类大小并不是0,编译器给了空类一个字节来唯一标识这个类的对象。
6.3类大小计算规则
遵循结构体对齐规则
对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
//VS中默认的值为8
//Linux中没有对齐数
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
7.this指针
7.1引入
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;
}
以上定义了一个Date类。我们知道类对象实例化后成员变量在其空间内,成员函数在一片公共区域,所以函数体内没有对不同对象的区分,那么当函数Init被d1对象调用时,被调用函数如何知道应该设置为d1而不是d2呢?这就引入了this指针来解决这个问题。
C++ 编译器给每个 “ 非静态的成员函数 “ 增加了一个隐藏 的指针参数,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 “ 成员变量 ” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成 。
7.2this指针详解
所有的成员函数都会多一个指针,这个指针就是this指针。它是成员函数的第一个参数,可以想象成下面的形式
与我们对栈进行初始化相同,函数传参时将定义的栈对象地址传过去,用指针接收。
所以this指针并不难理解,但它又有自己的一些特性:
①C++规定:把this指针叫做隐含的this指针,所有的成员函数第一个参数都是它,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递;
②this指针的类型:类类型* const ,即成员函数中,不能给this指针赋值;
③在形参和实参的位置,不能显示写出
④在函数内部可以使用
⑤this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
【面试中常见问题】
i虽然被const修饰,但 i 和 j 地址连续,是局部变量,都放在栈上;p是指针,也在栈上,但指针p所指的字符串在常量区;排除d;
2.this指针可以为空吗?
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
结果是C、正常运行,运行结果如图。
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
结果是B、运行崩溃,出现了this指针为空的情况,如下图