· CSDN的uu们,大家好。这里是C++入门的第九讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
1. 面向过程与面向对象
2. 类的定义
3. 类中的访问限定符
3.1 访问限定符的作用
4. 对象的实例化以及对象的大小
5. this指针
5.1 this指针的引入
5.2 VS中this指针的优化
5.3 考验你对this指针理解的两道题
我们之前学习的C语言是面向过程的语言,而我们正在学习的C++是面向对象的语言(OOP语言)。之前提到过,C++是对C语言的补充和改进。那么为什么C++要引入面向对象的概念呢?面向对象又比面向过程好在哪里呢?
面向过程,关注的是解决问题的步骤,将每一个功能都抽象出一个个具体的函数,然后逐步解决问题。
例如:我们在家洗衣服,站在面向过程的角度来看是这个样子:
通过一个一个的步骤来解决问题。在编程中就是通过一个一个的函数来实现。
面向对象:在解决问题的过程中,面向对象的思想是将解决该问题中的事物看作一个一个的对象,对象之间各司其职,达到解决问题的目的。
例如:在洗衣的过程中,人这个对象只需要将衣服放进洗衣机,等洗衣机洗好之后取出来即可。不需要关注洗衣机是怎么洗衣服的。
现在来分析面向对象与面向过程的优缺点还是太早了。我们只要有一个大致印象,面向对象比面向过程更加高级,面向对象能够更加方便的将问题模块化, 更好的解决问题。但是相比于面向对象,面向过程的代码执行效率较高。
我们知道C++是兼容C语言的,C语言的结构体里面是不能定义函数的。但是C++的结构体里面是可以定义函数的,因为C++将结构体升级成为了类,像这样:
struct A
{
int _a;
void func()
{
cout << "func" << endl;
}
};
int main()
{
struct A a1;
return 0;
}
C++将结构体升级成为了类,那么结构体的名字就是类型的名字,因此在C++中定义结构体是不需要加上struct的。
struct A
{
int _a;
void func()
{
cout << "func" << endl;
}
};
int main()
{
struct A a1;
A a1; //c++不用加struct
return 0;
}
在C++里面更喜欢用class代替struct来定义一个类,于是我们顺理成章地推导出了class定义类的方法:
class 类名
{
// 类的主体,包括成员变量和成员函数
};
注意:分号不能少。
// 定义一个类
class B
{
int _b;
void func()
{
cout << "func" << endl;
}
};
上面的代码我们定义了一个类 B。 类中的变量(_b),叫做成员变量,类中定义的函数(func) 叫做成员函数 或者方法。
C++规定:在类中的定义的函数,会被自动地视为inline函数,但是他最后是不是内联函数,还是取决于编译器。
注意:是在类中定义的函数,如果你是在类内声明,类外定义,不会被视为内联函数。
struct B
{
int _b;
void func() //函数的声明定义均在类里面
{
int a = 10;
}
void func(int a);
};
void B::func(int a)
{
int b = 10;
}
int main()
{
B b;
b.func();
b.func(1);
return 0;
}
上面的代码,func() 函数就是在类内定义的函数, 而func(int a) 则是在类外定义的函数,通过调试观察汇编代码,我们可以看到func()已经是一个内联函数了。
关于内联函数的细节:21天学会C++:Day6----内联函数_姬如祎的博客-CSDN博客
这里还有一个要注意的点:类中函数类外定义的写法,需要加上类名和域作用限定符,告诉编译器这是这个类里面的函数的实现。不然可能会与全局域的函数冲突。
class中的所有成员变量都在其所在的那个类域里面,这样做是理所应当的。
我们用struct定义了一个类 A,用class定义了一个类 B,创建一个变量之后。访问其各自的成员变量。发现struct定义的类可以直接访问,但是class定义的类不能直接访问成员变量。这是为啥呢?
struct A
{
int _a;
void func()
{
cout << "func" << endl;
}
};
class B
{
int _b;
void func()
{
cout << "func" << endl;
}
};
int main()
{
A a;
a._a = 10;
B b;
b._b = 10;
return 0;
}
这是因为C++中每一个类中的成员都会受到访问限定符的限制。我们来看看C++中的访问限定符有哪些:
其中public表示类成员在类内类外都可以访问;protected,private均是类成员在类内可以访问,在类外不可以访问。protected 与 private之间的区别需要我们学到继承的时候再讲。
于是我们就可以得出结论,在没有写访问限定符的时候,struct定义的类默认访问限定符是public;class定义的类默认访问限定符是private。
访问权限的作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。没有下一个访问限定符就是到类的结束位置。
这里需要uu们回忆一下我们在使用C语言实现的数据结构,无论哪一个都行。我们就拿栈来说吧,我们当时定义的栈是这样的:
C语言数据结构初阶(5)----栈_姬如祎的博客-CSDN博客
//栈的数据类型
typedef int STDataType;
//栈的结构体,类比顺序表
typedef struct Stack
{
STDataType* a; //栈的顺序存储的数组
int top; //栈的top元素
int capacity; //数组的容量
} ST;
这其中有一个top变量,我们在在实现栈的时候,提到过top可以指向栈顶元素或者栈顶元素的下一个位置,这取决于设计者的实现方式。
因为C语言结构体中的数据是公开的,于是,就会有程序员在访问栈顶的元素时写出这样的代码:
int main
{
ST s;
s.a[top];
}
是的,他不调用你实现的访问栈顶元素的函数,而是直接通过你的底层,直接访问数据。如果恰巧你实现的栈关于top的定义是实现方式2,碰巧他还是一个脾气暴躁的程序员,当他看到访问top的时候出现了随机值,他可能会直接破口大骂,这是谁写的 laji 代码。C语言没有常见数据结构的库也有一部分原因是这个吧。
没有访问限定符的限制,用户可以直接访问并修改任意数据,造成意料之外的结果,甚至导致程序崩溃。而有了访问限定符就能很好的解决这些问题。
我们只需要将stack的底层数据用private修饰就能很好的解决这些问题。同时将对应操作的函数用public修饰,提供对外接口供用户使用。
class Stack
{
public:
void StackInit()
{
//
}
void StackPush(int val)
{
//
}
//等等
private:
int* _a;
int _capacity;
int top;
};
这就是封装的具体表现了。
封装是面向对象的三大特性之一,另外两个是继承和多态。继承和多态后面讲解。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。 封装本质上是一种管理,让用户更方便使用类。
思考这样一个问题:你定义好了一个类,他是否在内存中占有空间呢?
答案显然是否定的,因为类是对象的描述,类在定义好时并不分配内存空间,只有在类实例化出对象之后,这个对象占有空间。我们完全可以把类当作一张图纸,而对象就是根据图纸生产出来的商品。
class A
{
public:
void func()
{
cout << "func" << endl;
}
private:
int _a;
int* _p;
};
int main()
{
A a;
return 0;
}
那么我们应该如何计算对象的大小呢?拿上面的代码来说,sizeof(a)结果是什么呢?
(这里写sizeof(A) 也行,根据对象能算出大小,根据类肯定也行撒,类比图纸与商品)
在32位机器下,结果是8。对象大小的计算方式和结构体大小的计算是一样的,都遵循内存对齐。
但是,你可能会问,成员函数存在哪里呢?对象里面没有成员函数是怎么调用的呢?
在回答这些问题之前,我们先来思考。不同的对象调用类中的函数是调用的同一个吗?没错调用的就是同一个。
既然所有的对象都会调用相同的成员函数,那么为什么还要浪费空间在每个对象里面存一份函数的地址呢,因此类的成员函数是存在公共代码段的,源文件编译的时候编译器会找到函数的地址,我们不必关心。
既然对象里面只存储成员变量,那要是我定义的类里面没有成员变量,那他还会有空间吗?
我们看到结果是1,如果没有成员变量就没有空间的话,我们应该用什么来表征这个对象的存在呢?因此即使没有成员变量的类,实例化出的对象也是有空间的,至于具体的大小,依编译器而定。
我们创建了一个Date 类,类的成员变量用来存储年月日。InitiDate函数用来初始化一个Date对象的日期,ShowDate用来打印Date对象表示的日期。
class Date
{
public:
void InitDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void ShowDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.InitDate(2004, 01, 01);
d1.ShowDate();
Date d2;
d2.InitDate(2008, 01, 01);
d2.ShowDate();
}
我们上一个小节讲到了一个类实例化出来一个对象就会开辟一份属于自己空间,那么上述代码中的d1,d2是两个不同的对象,也就各自拥有一份空间来存储各自表示的日期。但是类的成员函数只有一份,他是怎么做到不同的对象调用同一个函数时,找到不同空间中的数据(d1调用ShowDate打印的是d1中存储的信息,d2调用ShowDate打印的是d2中存储的信息)的呢?
这就得讲讲我们的this指针了,C++语法规定,对于非静态成员函数(没有加static修饰的成员函数),编译器会对成员函数做处理:在普通成员函数中加了一个隐藏的指针参数,让这个指针指向当前对象( 正在调用这个函数的对象,d1调用ShowDate,this指向的就是d1这个对象)。需要注意的是this指针不能在形参和实参显示传递,但可以在函数内部显示使用。
例如,ShowDate函数可以这样写:
void ShowDate()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
现在我们就能理解为什么同一个函数能访问不同的空间了吧,因为隐藏传递了一个this指针,打印_year等变量,实际上是通过传递过来的this指针找到调用该函数的对象中的数据。因此才能做到一个函数访问两块空间。
那这里我就要问一个问题了,各位uu觉得this指针存储在哪里呢?
A:对象 B:栈 C:堆
答案:B。
A:如果this指针存在对象中,那么我们刚才计算对象大小的时候并没有计算this指针哇,因此排除a。
我们再来看看this的定义嘛,this是对象这个实参传递过来的,那么this就是形参撒,uu们形参当然是存在栈中的撒。因此this就是存在栈中的!!!
我们来看看VS对this指针的优化:我们通过反汇编来看看this指针的传递
我们可以看到,VS中直接将this指针存在了寄存器中,我们都知道寄存器的读写速度是非常快的,在类的内部,我们需要大量访问成员变量。而访问成员变量本质上是通过this指针来访问的。因此将他存储到ecx寄存器中能够提高访问的效率。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
我们看到这两道题都是通过一个空对象去调用函数,区别是在函数中是否访问对象中的成员变量。
答案:1:运行正常
2:运行崩溃
在调用类成员函数的时候,会隐士传递this指针,都传递的是一个空指针,题目一并没有对this指针解引用,但是题目二确尝试解引用当问成员变量。因为操作成员变量的本质都是通过this指针,因此题目二会发生空指针的解引用,引起程序崩溃。