C++类和对象详解(一)
✅6000字详细介绍类和对象
✨MENU✨
0. 前言
1. 类的引入
在C语言中,结构体中可以定义成员变量。
struct MyStruct {
int a;
int b;
};
创建结构体变量,通过 结构体变量名.结构体成员
可以访问其内部成员。
struct MyStruct st;
st.a = 1;
st.b = 2;
在C++中,定义结构体对象时,可以省去关键字struct
,直接使用结构体名。
//C
struct MyStruct st;
//C++
MyStruct st;
以上结构体struct
的定义,C++中更多用类class
代替。
在C语言中,结构体中只能定义变量。在类中不仅能定义变量,还能定义函数 。
同结构体一样,类的定义为 class
+ 类名
。
class MyClass {
public:
void function();
private:
int i;
}; //类的定义
用类
类型创建对象的过程,称为类的实例化
。
MyClass A; //类的实例化
C++使用访问限定符
限定类中成员的访问,分为以下三种:
访问限定符 | 说明 | 访问权限 |
---|---|---|
public | 公有的 | 在类外可以直接访问 |
private | 私有的 | 只能被类中的成员函数访问,类外无法直接访问 |
protected | 保护的 | 能被类中和子类的成员函数访问,类外无法直接访问 |
一个类中可以有多个访问限定符标识,访问限定符的作用范围到下一个访问限定符或类定义结束}
为止。
class MyClass {
public:
void func1() {
cout << "qwq\n";
}
private:
int a;
char str[10];
public:
static int i;
};
与结构体不同的是,结构体实例化的对象成员默认访问权限为
public
,而类实例化的对象的成员访问权限默认为private
,无法从外部直接访问。
类定义的代码排版:三个访问限定符
public:
、private:
、protected:
与该类定义位置的class
保持同一缩进且单独占一行,类中成员则向后缩进,如上所示。
类定义了一个域名为类名的作用域,类中的成员都在这个作用域中。在类外访问类内成员时需要使用作用域解析操作符::
。
class A{
public:
static void fun(){
cout << "qwq\n";
}
};
//在类外调用类A中的公有静态成员函数fun
A::fun();
通过::
访问成员,属于从类外访问类内成员,因此只能访问public
限定的成员,而不能访问private
或protected
限定的成员。
关于static
修饰成员的使用,在之后的章节会讲到。
2. 类对象在内存中的储存形式
类中同时存在成员变量和成员函数,那么类实例化的对象在内存中是如何储存的呢?以下面的类为例。
class A {
public:
void fun1(){
cout << 1;
}
void fun2(){
cout << 2;
}
private:
int a;
int b;
};
用sizeof
计算类的大小和类实例化的对象的大小:
A a1;
cout << sizeof(A) << endl;
cout << sizeof(a1) << endl;
输出结果:
8 8
可以发现,此时类的大小等于类中的成员变量大小之和,因此可以得出:类在实例化的时候,并没有包含类中的成员函数。
事实上,类实例化的对象,只会将类中成员变量按照结构体对齐规则储存,而类中的成员函数则保存在公共代码区,不占用对象的空间,如上图所示。
另外,如果是空类或者类中不存在非静态类型的成员,则这种类实例化的时候编译器会给一个字节来标识这个对象。
3. 类的成员函数
成员函数可以定义在类中,也可以声明在类中定义在类外。
class A {
public:
//类中声明
void fun1();
private:
void fun2();
int _a;
int _b;
};
//类外定义
void A::fun1() {
cout << _a;
}
void A::fun2() {
cout << _b;
}
需要注意的是,在类外部定义的函数,需要在函数名前加上类域名
和作用域解析操作符::
。类外部也可以定义类中用private
和protected
限定声明的函数。
成员函数的调用,可以使用对象.成员函数()
的方式,如下:
class A {
public:
void fun() {
cout << 1;
}
void init(int a) {
_a = a;
}
private:
int _a;
};
int main() {
A a1;
a1.fun(); //调用成员函数fun
a1.init(3); //调用成员函数init
return 0;
}
也可以用匿名对象的方式调用成员函数,如下:
class A {
public:
void print() {
cout << "qwq\n";
}
};
int main() {
A().print();
return 0;
}
对象实例化时,通常要有对象的名字,比如A a1;
。上面的代码中,A()
是一个匿名对象,因为没有名字,所以之后就无法施用,其生命周期只有A().print();
这一行。
在上面的代码中,对象a1
是被定义在main
函数中的,因此a1
的成员变量_a
也是储存在main
函数的栈帧中的。但是在a1.init(3);
中,对象a1
中的成员变量_a
在函数init
中被改变了,从表面上看,对象a1
并没有传参到init
中,这是怎么回事呢?
通过 Visual Studio 2019 调试转到反汇编,可以发现在执行语句a1.init(3);
时,不仅传了参数3
,还传了对象a1
的地址。
再查看函数void init(int a)
的反汇编,可以发现它用一个名为this
的指针接收了传过去的对象a1
的地址。
可以发现,函数init
内部其实是通过this
指针修改了_a
的值。
事实上,类的每个非静态成员函数中都隐藏了一个this
指针,该指针指向当前调用的对象,被作为成员函数的第一个参数。
在函数void init(int a);
中,实际的参数表为void init(A* const this, int a);
。在传参时,a1.init(3)
实际上传的是a1.init(&a1, 3)
在C++中,使用没有const
修饰的指针,不能直接接收const
修饰的常量地址。如果要用指针接收变量的地址,则必须用const
修饰指针变量。
同样的,类的成员函数中,第一个参数this
指针默认是不被const
修饰的,比如下面的类,它有一个隐藏的参数A* const this
。
class A {
public:
void fun() {
// void fun(A* const this) <-实际的形式
cout << _a << endl;
}
int _a;
};
由于const
在*
之后,所以并不能限制*this
的修改。因此,如果向该函数中传入const
修饰的A
类型对象,则会报错:
要解决这个问题,我们必须用const
修饰this
指针,即const A* const this
,但是this
指针是一个隐藏的参数,无法直接在该参数上修饰,怎么办呢?
使用
const
在函数圆括号后修饰,即可修饰隐藏的this
指针。表示该成员函数不能修改任何对象中的成员变量。
class A {
public:
void fun() const {
// void fun(const A* const this) <-实际的形式
cout << _a << endl;
}
int _a;
};
当this
为空指针时,只要this
不发生解引用,程序依然能正常运行:
class A {
public:
void print() {
cout << "qwq\n";
}
private:
int _a;
};
int main() {
A* pa = nullptr;
pa->print();
return 0;
}
虽然pa
是个空指针,但函数print
并不是在对象中储存的,且传入的this
指针没有发生解引用操作。因此以上代码能够正常编译运行。
this
指针也可以作为返回值使用,观察下面代码中的add
函数:
class A {
public:
void init(int a = 0) {
_a = a;
}
A& add() {
++_a;
return *this;
}
void print() {
cout << _a << endl;
}
private:
int _a;
};
int main() {
A a;
a.init(0);
a.print();
a.add().add();
a.print();
return 0;
}
在语句a.add().add();
中,因为this
指针将对象本身作为返回值引用返回,所以a.add()
的返回值可以作为对象再次调用add()
。
输出结果:
0 2
4. static修饰的成员
在C语言中,全局变量和用static
修饰的静态变量在内存中都是储存在静态区中的。
int a = 0; //全局变量 储存在静态区
void fun(){
int a = 10; //局部变量 储存在栈区
int* p = (int*)malloc(sizeof(int));
*p = 20; //在堆中开辟的空间 储存在堆区
static int s = 30; //静态变量 储存在静态区
}
在C++中,成员变量的类型允许用static
修饰为静态成员变量,静态成员变量也储存在静态区,不占用对象的内存空间。因此,静态成员变量是该类类型所有对象共享的,不属于某个具体对象。
//在类中声明静态成员变量
class A {
public:
static int _a;
private:
int _i;
static int _b;
};
静态成员变量必须在类外定义初始化,定义时需要指明类域,并使用作用域解析操作符::
,不需要使用static
修饰。仅在定义时,可以在类外初始化private
或protected
限定的静态成员变量。
int A::_a = 10;
int A::_b = 20;
对于public
修饰的成员变量,普通的成员变量可以使用对象访问,静态成员变量不仅可以使用对象访问,还以通过类域名和作用域解析操作符::
访问。
class A {
public:
static int _s1;
static int _s2;
int _i;
};
int A::_s1 = 0;
int A::_s2 = 0;
int main() {
A a;
//通过对象可以访问普通成员变量和静态成员变量
a._i = 10;
a._s1 = 20;
//通过类域可以直接访问静态成员变量
A::_s2 = 30;
// A::_i = 40; //错误的 不能通过类域访问普通成员变量
return 0;
}
如上,通过A::_s2
不创建对象直接访问静态成员变量。
在C++中,不仅可以用static
修饰成员变量,还能修饰成员函数。
静态成员函数与其他成员函数不同的是,其参数表中没有this
指针,因此静态成员函数不能访问非静态成员变量,且和静态成员变量一样可以通过类域名和作用域解析操作符::
访问。
class A {
public:
static void print() {
cout << _a << endl;
// cout << _i << endl;
// 错误的 静态成员函数没有this指针,无法访问非静态成员变量
}
private:
static int _a;
int _i;
};
int A::_a = 1;
int main() {
A::print(); //通过类域直接访问静态成员函数
return 0;
}
static
成员总结:
成员变量 | 在内存中的储存位置 | 访问方式 | 初始化 | 是否共享 |
---|---|---|---|---|
非静态 | 对象储存的位置 | 只能通过对象访问 | 只能通过对象初始化 | 每个非静态成员变量都是独立的 |
静态 | 静态区 | 可以使用类域访问 | 在类外全局初始化 | 该类的对象共享同一个静态成员变量 |
成员函数 | 调用方式 | 有无this 指针 | 访问静态成员变量 | 访问非静态成员变量 |
---|---|---|---|---|
非静态 | 只能通过对象调用 | 有 | 能 | 能 |
静态 | 可以使用类域调用 | 无 | 能 | 不能 |
5. 友元
由前面的知识可知,类外的函数无法访问类中private
和protected
限定的成员变量。如果要让类外函数能访问,需要在类中声明该函数为友元函数,即在函数声明前加上关键字friend
。此时该函数虽然不是类的成员函数,但能访问类的非public
成员。
class A {
public:
//声明友元函数
friend void fun(A& a);
private:
int _i;
};
void fun(A& a) {
//在非成员函数中访问类的私有成员变量
a._i = 1;
}
友元函数是定义在类外的普通函数,不属于任何类的成员,没有this
指针,因此不能使用const
修饰。它可以同时是多个类的友元,可以在类定义的任何地方声明,不会受到访问限定符的限制。
需要注意的是,友元函数会破坏类的封装,增大耦合度,所以不宜多用。
在一个类中,如果用friend
修饰声明另一个类,则称另一个类是这个类的友元类。友元类使用 friend class
+ 类名
声明。
如下代码,B
是A
的友元类,所以B
中的成员函数可以访问A
中的非public
成员。
class A {
public:
friend class B;
private:
int _a;
};
class B {
public:
void fun() {
A a1;
//因为B是A的友元类,所以可以直接访问A中私有的_a成员
a1._a = 1;
}
private:
int _b;
};
友元类是单向的,以上的代码中,B
中能访问A
的非public
成员,但是A
中不能访问B
的非public
成员。
友元类不具有传递性,如下代码:B
是A
的友元类,即B
中能访问A
的成员,C
是B
的友元类,即C
中能访问B
的成员,但是C
和A
没有友元关系,因此C
中不能访问A
中的成员。
class A {
public:
friend class B;
private:
int _a;
};
class B {
public:
friend class C;
private:
int _b;
};
class C {
public:
void fun() {
A a1;
// a1._a = 1;
// 错误的 A和C之间没有友元关系
}
private:
int _c;
};
如果一个类定义在另一个类的内部,则这个类天生就是另一个类的友元。
class A {
public:
//B定义在A的内部
class B {
public:
void fun() {
cout << _i << endl;
A a1;
//B是A的友元
a1._a = 1;
}
};
private:
int _a;
static int _i;
};
由于B
只是被定义在A
类中,因此计算sizeof(A)
时,并不会计算B
中的成员变量大小。
下一篇:C++类和对象详解(二)