C++中的类(class)是实现数据抽象和面向对象程序设计的核心。本文作为类和对象的开篇,将介绍有关类的基础知识,之后会持续更新类和对象的深入内容。
目录
前言
1 类的引入
2 类的定义
3 访问限定符和封装
3.1 访问限定符
3.2 封装
4 类的实例化及类对象的储存
4.1 实例化
4.2 类对象的储存
4.3 类的大小计算
5 this指针
5.1 this指针的引入
5.2 this能否为空指针
5.3 代码规范
C++兼容C语言,C语言中的结构(struct)在C++中扩充成了类。
struct Stack
{
// ...
};
struct Stack st1; // C语言定义结构体变量
Stack st2; // C++创建类对象
C语言结构体中只能定义变量,在C++中,结构体不仅可以定义变量,也可以定义函数。
struct Stack
{
// 成员函数
void Init()
{
a = nullptr;
top = capacity = 0;
}
void Push(int x)
{
// ...
}
// 成员变量
int* a;
int top;
int capacity;
};
Stack st;
st.Init(); // 使用:类对象.成员变量/成员函数
st.Push(1);
class className
{
// 类体
};
class为定义类的关键字,className为类的名字,{}中为类的主体。类体中的内容为类的成员:成员变量(类的属性),成员函数(类的方法)。
类域:类定义了一个新的作用域,类的所有成员都在类域中,在类体外定义的成员需要作用域限定符::指定它属于哪个类域。
类的定义方式:
1.声明和定义全部放在类体中。成员函数在类中定义可能会被编译器当作内联函数处理。
2.类声明放在.h文件中,成员函数定义放在.cpp文件中(成员函数前需加 类名::)
访问限定符分为:public(公有),protected(保护)和private(私有)
说明:
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
// .h文件中声明成员函数
class Stack
{
public:
void Init();
void Push(int x);
int Top();
private:
int* a;
int top;
int capacity;
};
// .cpp文件中定义成员函数
void Stack::Init()
{
a = nullptr;
top = capacity = 0;
}
void Stack::Push(int x)
{
if (top == capacity)
{
int newCapacity = (capacity == 0 ? 4 : capacity * 2);
int* tmp = (int*)realloc(a, sizeof(int) * newCapacity);
if (tmp == nullptr)
{
perror("relloc fail");
return;
}
a = tmp;
capacity = newCapacity;
}
a[top++] = x;
}
int Stack::Top()
{
return a[top - 1];
}
int main()
{
Stack st;
int top = st.Top();
// st.a[st.Top] 不能访问
return 0;
}
如上定义栈类(Stack),成员变量top初始为0指向栈顶元素下一个位置,用户可能认为top指向栈顶元素,通过访问成员变量获取栈顶元素(st.a[st.top])出错,因此通过将Stack进行封装,将类的属性设为私有,用类的方法Top()实现获取栈顶元素供用户使用。
用类创建对象的过程称为类的实例化。类就像是一种模型,对对象进行描述,限定了类有哪些成员,定义一个类并没有分配实际的空间来储存它,一个类可以实例化出多个对象,实例化出的对象才占用实际的物理空间。
定义一个类,它的成员变量实际是一种声明,只有实例化出的对象才会定义这些成员变量,因此实例化类对象需要开辟物理空间储存成员变量,那么类的成员函数需要每个对象储存吗,类的成员函数不仅要在类中声明,也要在类中定义,每个对象都可以使用类的成员函数,所以类的成员函数不需要实例化对象储存,事实上它们也不需要类储存,因为它们存放在公共代码区。
那么计算类的大小只需要计算类中的成员变量,其计算方式遵循结构体内存对齐规则。
结构体内存对齐规则:
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如创建三个类,它们分别是空类、只含成员函数的类、只含成员变量的类、含成员变量和成员函数的类,分别计算它们的大小。
// 空类
class A
{
};
// 类中仅有成员函数
class B
{
public:
void Func()
{
}
};
// 类中仅有成员变量
class C
{
private:
int a;
int b;
int c;
};
// 类中既有成员函数又有成员变量
class D
{
public:
void Func()
{
}
private:
int a;
int b;
int c;
};
int main()
{
cout << "sizeof(A) " << sizeof(A) << endl;
cout << "sizeof(B) " << sizeof(B) << endl;
cout << "sizeof(C) " << sizeof(C) << endl;
cout << "sizeof(D) " << sizeof(D) << endl;
return 0;
}
结果:
C类含3个整型其大小为12字节,D类和C类的大小相同,可以验证成员函数不需要类储存,A类和B类的大小都为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;
Date d2;
d1.Init(2022, 7, 20);
d2.Init(2022, 7, 21);
return 0;
}
如上定义一个日期类,创建两个对象d1,d2,类中的函数没有关于不同对象的区分,当d1调用Init()函数时,函数是如何将参数传给对象d1的成员变量(_year,_month,_day)而不是对象d2的呢?
C++中引入this指针解决该问题,即:类的每个成员函数有一个隐藏的指针参数this,当对象调用函数时会将对象的地址传给函数的this指针,函数内所有成员变量的操作都通过this指针去访问。this指针的设置和传参由编译器自动完成,用户不能显式写出,在函数体内可以使用。
this指针的特性:
函数调用实际实现如下:
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* const this)
{
cout << this->_year << '/' << this->_month << '/' << this->_day << endl;
}d1.Init(&d1, 2022, 7, 20);
d1.Print(&d1);
通过验证,发现:对象d1,d2调用Init()和Print()函数时,函数的this指针的值和它们本身地址相同。
在一般情况下,不能对空指针进行解引用操作,所以类对象的指针不能为空,this指针也就不能为空。如果类的成员函数中没有对this指针的实际解引用,即便给this传空指针也不会出现问题。
如下程序会正常执行:
p->Print()或(*p).Print()好像是对空指针p进行了解引用,实际上它调用Print()函数时会直接把p传给this,因为实参&(*p)和p是相同的。这种情况在实际中可能很少出现,但它是可行的。
如下程序就会运行崩溃:
因为Print()函数内对成员变量进行操作,访问_a需要对this指针进行解引用,this->_a,对空指针解引用引起运行崩溃。
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;
};
执行以上程序,输出的却是随机值,说明对象d1调用了Init()函数但是d1的成员变量并没有初始化,这是为什么呢?
其原因是函数Init()内的year,month,day变量都是局部变量(函数形参),比如year = year,我们期望左边的year是类的成员变量year,右边是函数形参year,事实上左边的year也是函数的形参,因为year是存在的局部变量,函数内的year自然都优先与它对应,编译器不会“智能”地对左边的变量使用this->访问,它就不会对应类的成员变量。以上操作如同定义一个变量int a;,再赋值变量给它自己 a = a;
因此要实现我们的期望,就有两种方法:显式使用this指针或改变变量名。
通过显式使用this指针,将左边的变量设为类的成员变量,即:
void Init(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
将类成员函数的参数与类成员变量书写不同就能避免this指针失灵的问题。常见的代码书写规范是在类的成员变量前或后加_,即:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
如果要在函数内使用全局变量,其参数也不能和全局变量重名,否则会将全局的名字隐藏,因此规范的变量命名是解决一些问题的通用方法,一般不使用第一种方法(显示使用this指针)。
如果本文内容对你有帮助,可以点赞收藏,感谢支持,期待你的关注。
下篇预告:C++ 类和对象(二)构造函数、析构函数、拷贝构造函数