C++基本语法
一;类和对象
1.1;如何定义一个类?
在C++语言当中,我们可以使用关键字“struct”和“class”来定义。
//struct关键字定义一个类
struct Person
{
//成员变量
int m_age;
//成员函数
void run() {
cout << m_age << " --- > run()" << endl;
}
};
//class关键字定义一个类
class Person
{
//成员变量
int m_age;
//成员函数
void run() {
cout << m_age << " --- > run()" << endl;
}
};
1.2;关键字“struct" 和“class"定义类有什么区别?
struct关键字定义的成员权限默认为public。
//struct关键字定义一个类
struct Person
{
//成员变量
int m_age;
//成员函数
void run() {
cout << m_age << " --- > run()" << endl;
}
};
//总结:struct定义的类成员可以被对象/指针任意调用。
int main() {
//实例化一个类
Person student;
//使用指针来指向对象
Person* q = &student;
//调用成员变量
q->m_age = 10;
//调用成员函数
q->run();
}
class关键字定义的成员权限默认为private。需要手动添加为public:才能被任意调用
//struct关键字定义一个类
class Person
{
public:
//成员变量
int m_age;
//成员函数
void run() {
cout << m_age << " --- > run()" << endl;
}
};
//总结:struct定义的类成员可以被对象/指针任意调用。
int main() {
//实例化一个类
Person student;
//使用指针来指向对象
Person* q = &student;
//调用成员变量
q->m_age = 10;
//调用成员函数
q->run();
}
1.3;如何实例化对象(创建一个对象)?
我们可以使用类名+变量名来新建一个对象。
int main() {
//实例化一个类
Person student; // Person是类名,student为变量(对象的名称)。
}
1.4; 使用指针来指向一个对象
//实例化一个类
Person student; // Person是类名,student为变量(对象的名称)。
//使用指针来指向对象
Person* p = &student;
1.5;如何调用类当中的成员?
1.5.1;使用对象来调用
使用“.”来调用类当中的成员
int main() {
//实例化一个类
Person student; // Person是类名,student为变量(对象的名称)。
//调用成员变量
student.m_age = 1;
//调用成员函数
student.run();
getchar();
return 0;
}
1.5.2;使用指针来调用对象当中的成员
int main() {
//实例化一个类
Person student; // Person是类名,student为变量(对象的名称)。
//使用指针来指向对象
Person* q = &student;
//调用成员变量
q->m_age = 10;
//调用成员函数
q->run();
}
二;对象的内存
2.1;对象内存大小
注释:这里没有讨论类当中出现指针且指向堆空间的情况。如果出现指针指向堆空间的情况,内存大小需要加上指针的大小和申请堆空间的大小。
对象的大小本质上取决于类当中的成员成员变量的数量与大小。我们用下面代码来说明。(成员函数在代码段中,没有放入栈空间)
#include <iostream>
using namespace std;
class Person
{
public:
int m_age;
void run() {
cout << m_age << " --- > run()" << endl;
}
};
int main() {
Person student;
Person* q = &student;
q->m_age = 10;
q->run();
}
如上代码当中我们发现在"Person"类当中定义了一个int类型(4字节)的成员变量。接下来,我们来反编译查看一下,详细如下面代码:
14: Person student;
15: Person* q = &student;
00007FF6C845235B lea rax,[rbp+4] //栈空间为对象划分了4字节的空间
00007FF6C845235F mov qword ptr [rbp+28h],rax
现在,我们在“Person”类当中新增一个成员变量int类型。看看接下来栈空间会划分多大的空间。
class Person
{
public:
int m_age;
int m_age2;
void run() {
cout << m_age << " --- > run()" << endl;
}
};
添加之后,我们发现,栈空间划分了8字节的空间,总结:对象的大小本质上取决于类当中的成员成员变量的数量与大小
15: Person student;
16: Person* q = &student;
00007FF64D70235B lea rax,[rbp+8]
00007FF64D70235F mov qword ptr [rbp+28h],rax
2.2;对象内存的位置
对象内存存放在数据段(全局区)
#include <iostream>
using namespace std;
class Person
{
public:
int age;
};
Person student; //在函数外创建对象,对象内存在数据段
对象内存存放在栈空间
#include <iostream>
using namespace std;
class Person
{
public:
int age;
};
int main() {
Person student; //在函数内创建对象,对象内存在栈空间
}
对象内存存放在堆空间
#include <iostream>
using namespace std;
class Person
{
public:
int age;
};
int main() {
Person *p = new Person; //申请一个堆空间,Person类类型对象,对象内存放在堆空间
}
三;THIS
3.1;this的作用
this是什么?在类当中我们经常会看到这么一个关键字。下面我们通过一串代码来理解一下。我们来看一下这个代码它有一个类“Person”。里面有一个方法run()。方法当中是一个输出。其中包含了this这个关键字。在main函数当中,我们实例化了两个对象student,student2。在两个对象当中都对m_age进行了赋值,分别是2,3。
#include <iostream>
using namespace std;
class Person
{
public:
int m_age = 1;
void run() {
cout << "m_age = " << this->m_age << endl;
}
};
int main() {
Person student;
Person student2;
student.m_age = 2;
student2.m_age = 3;
student.run();
student2.run();
}
运行代码结果如下图,我们从结果上来看,对象student输出的值和student2输出的值分别为2和3这正是刚刚student.m_age=2,student2.m_age=3的结果。我们现在在来看看类当中的“this->m_age”有什么想法呢?是不是在对象student当中“this->m_age”就等于“student.m_age”,对象student2当中“this->m_age”就等于"student2.m_age"。
结论:this表示的是当前调用者对象的指针

3.2;this的原理
现在我们已经知道了this是什么。那么接下来,我们用如下代码来看一下this的原理。它为什么能做到这些。
#include <iostream>
using namespace std;
class Person
{
public:
int m_age = 1;
void run() {
this->m_age = 5;
}
};
int main() {
Person student;
student.run();
}
我们在“student2.run()”位置设置断点,进行反汇编来查看一下。首先,我们看到如下指令。这是C++语言“student2.run()”对应的汇编代码。我们发现,首先它将student2对象的地址放入了rcx当中。接下来call指令执行地址00007FF677B0147E的函数。
16: student.run();
00007FF677B01B64 lea rcx,[rbp+4] //rbp+4即student对象的地址
00007FF677B01B68 call 00007FF677B0147E
下面,我们执行f11,进入00007FF677B0147E地址的函数看看发生了什么事情。进入之后,我们发现,00007FF677B0147E地址保存的是一个jmp指令,且该指令跳转的目的地址是Person::run。即类当中的run()方法。
00007FF677B0147E jmp Person::run (07FF677B01F50h)
我们继续单步执行,追踪一下代码,这个时候,我们发现跳转到了类当中的run()函数位置了。如下代码,我们发现首先执行mov 指令将rbp+e0h的位置的值放入rax。然后将数值5放入[rax]当中。由c++代码“this->m_age=5”我们很容易猜测rbp+e0h 就是this的指针。那么,我们来看看rbp+e0h位置存放的内容和对象student2有什么关系呢?
11: this->m_age = 5;
00007FF677B01F86 mov rax,qword ptr [rbp+00000000000000E0h]
00007FF677B01F8D mov dword ptr [rax],5
我们来回顾一下,之前在"student.run()"代码中,将student对象的地址放入rcx寄存器当中的,那么我们查看如下代码,我们发现rcx寄存器当中的student对象的地址放入了[rsp+8]的位置。我们通过软件查看发现[rsp+8]的值等于[rbp+e0]。到了这里我们知道了this的指针当中保存着是对象student的地址。
10: void run() {
00007FF677B01F50 mov qword ptr [rsp+8],rcx
总结:在执行student.run()代码是,将student对象的值保存到寄存器,然后由寄存器压入栈当中,在调用this的时候,会将保存如栈的student对象的地址放入this指针当中。即完成了this表示当前调用者的需求
四;指针访问的本质
指针通俗的来说是指一段保存着地址的内容空间。那么我们用它来访问数据的本质是什么呢?我们使用下面代码来理解一下。整个代码很简单,类“Person”当中定义了三个成员变量,在main函数当中实例化一个对象且使用指针来访问成员变量并赋值。
class Person
{
public:
int m_age1;
int m_age2;
int m_age3;
};
int main() {
Person student;
Person* p = &student;
p->m_age1 = 1;
p->m_age2 = 2;
p->m_age3 = 3;
}
我们反汇编来查看一下。
12: Person student;
13: Person* p = &student;
00007FF7EBCC1F8B lea rax,[rbp+8] //student对象地址放入rax寄存器当中
00007FF7EBCC1F8F mov qword ptr [rbp+38h],rax //student对象地址放入[rbp+38h]地址当中。rbp+38为指针P
14: p->m_age1 = 1;
00007FF7EBCC1F93 mov rax,qword ptr [rbp+38h] //将student对象地址放入rax寄存器当中,此时[rax]=student地址
00007FF7EBCC1F97 mov dword ptr [rax],1 //将1赋值给m_age1
15: p->m_age2 = 2;
00007FF7EBCC1F9D mov rax,qword ptr [rbp+38h]
00007FF7EBCC1FA1 mov dword ptr [rax+4],2 //将2赋值给m_age2
16: p->m_age3 = 3;
00007FF7EBCC1FA8 mov rax,qword ptr [rbp+38h]
00007FF7EBCC1FAC mov dword ptr [rax+8],3 //将3赋值给m_age3
五;封装
封装是一个什么样的概念呢?上文我们提到使用关键字"class"来定义一个类,它默认的权限是private。这个时候其中的成员变量是没有办法被实例化后的对象访问的。这个时候我们在类当中提供public的方法(可以在方法中加入参数限定条件)来访问成员变量。这样的操作我们称之为封装。下面,我们用一段代码体会一下。代码中使用的是calss关键字定义。所以直接实例化对象之后没有办法调用成员变量。所以类当中写入了两个函数用来提供传参和查看的功能。
#include <iostream>
using namespace std;
class Person
{
int m_age1;
public:
void setAge(int age) {
if (age <= 0) {
m_age1 = 1;
}
else
{
this->m_age1 = age;
}
}
int getAge() {
return m_age1;
}
};
int main() {
Person student;
Person* p = &student;
p->setAge(100); //通过成员函数setAge()给成员变量m_age1输入值。查看是否能传输
cout << p->getAge() << endl;
p->setAge(-100); //通过成员函数setAge()给成员变量m_age1输入值。查看是否能传输,且是否有判定功能。
cout << p->getAge() << endl;
}
六;内存空间
首先,我们每一个程序使用的空间都是一个连续的一维的空间地址,在32bit操作系统当中,每个程序都占有4GB的运行空间。(注意:这里的空间地址是虚拟地址)那么,在这样虚拟空间当中具有哪几个部分呢?一般具有如下几个区域:代码段,数据段,栈空间,堆空间。
代码段:存放代码的位置。
数据段:保存数据,比如全局变量等。
栈空间:特殊的保存数据的内存空间,遵寻先入后出原则。常常用来调用一个函数时分配一段连续的空间来保存局部变量。
堆空间:程序主动申请和释放的空间。
七;堆空间
堆空间的特点是可以自由的控制内存的生命周期,大小。也就是说,堆空间的申请和释放都需要主动操作,程序不会自动完成。
7.1;malloc/free申请堆空间/释放堆空间
那么,我们该如何去申请堆空间呢?在C++代码可以使用“malloc”来申请堆空间,注意该函数的返回值是请求堆空间的首地址。详细如下
malloc(4) //malloc函数申请了4个字节的连续的堆空间,并且将第一个字节的地址返回
堆空间申请完成,接下来,我们给如何利用了。正常我们需要使用指针来使用堆空间。现在我们还要思考一个问题就是,我们该如何的定义指针的类型。malloc(4)将四个字节的空间取出来,至于我们将它如何看待,是int类型,还是char类型,取决于我们怎么思考。但是,我们需要强转换一下。详细如下:
int *p = (int *)malloc(4) //我们需要将申请的堆空间看作int类型数据,那么使用"(int *)"来强制转换。放入int 类型指针。
下面,我们在思考一个问题,我们知道char 类型是占用一个字节,那么“char *p = (char *)malloc(4)”会是一个什么样的情况呢?我们接下来输入“*p = 10”又会是一个什么样的光景呢?
解答上面问题,我们先思考一下malloc(4)为我们申请了四个字节的空间,现在我们需要将他当成char类型来使用,那么等同于我申请了4个char类型的数据。在来回顾一下,malloc返回了第一个字节的地址。那么“*p = 10”实际上是将“10”放入了第一个字节当中。
接下来是,余下的三个字节我们该怎么使用呢?还记得上文“四;指针访问的本质”这个章节嘛?是的,我们可以通过偏移量来访问下面的三个字节。如下代码说明:
char *p = (char *)malloc(4)
*p = 10 //第一个字节赋值10
*(p+1) = 11 //第二个字节赋值11
*(p+2) = 12 //第三个字节赋值12
*(p+3) = 13 //第四个字节赋值13
//其他字节访问的另一种写入手法
p[0] = 10 //第一个字节赋值10
p[1] = 11 //第二个字节赋值11
p[2] = 12 //第三个字节赋值12
p[3] = 13 //第四个字节赋值13
之前我们提到,堆空间的释放是不会由程序自动执行的,需要我们使用free()来执行操作。详细如下:
int *p = (int *)malloc(4) //申请堆空间
free(p) //释放堆空间
7.2;new/delete申请和释放堆空间
new,delete两个函数配套使用的申请和释放堆空间的函数,操作方法如下:
注意:new和delete,new [] 和 delete[] 不能交互使用。
int* r = new int; //申请一个int类型的堆空间
*r = 10;
delete r; //删除堆空间
int* f = new int[4]; //申请四个int类型的堆空间
*f = 10;
delete[] f; //删除堆空间
八;构造函数
注释:通过malloc生成的堆空间对象无法自动调用构造函数
构造函数是指在对象创建的时候自动调用的函数,一般用于初始化操作。构造函数与类名相同,无返回值(无须void),可以由参数,支持重载。详细如下:
8.1;无参数构造函数
#include <iostream>
using namespace std;
class Person
{
public:
int age;
Person() { //构造函数
age = 1;
}
};
int main() {
Person* p = new Person;
cout << p->age << endl; //输出成员变量age的值,发现在创建对象的过程中,自动调用构造函数给age进行赋值
}
8.2;有参数构造函数
那么,如果我们希望初始化的值由我创建过程中设定,该如何处理呢?请用下面代码理解
#include <iostream>
using namespace std;
class Person
{
public:
int age = 0;
Person(int age) { //构造函数允许传参
this->age = age;
}
void display() {
cout << this->age << endl;
}
};
int main() {
Person student(20); //创建对象过程中输入参数
student.display();
}
九;析构函数
注释:通过malloc生成的堆空间对象无法自动调用析构函数
既然有创建对象自动运行的函数,那么对象销毁的时候是否也是有自动调用函数呢?答案是有的。它的名字叫析构函数。析构函数的特点是以“~“开头,与类同名,无返回值(无void),无参,无重载,有且只能有一个。下面我们用代码来感受一下:
#include <iostream>
using namespace std;
class Person
{
public:
int age = 0;
Person(int age) { //构造函数
this->age = age;
}
~Person() { //析构函数
cout << "~Person()" << endl;
}
void display() {
cout << this->age << endl;
}
};
int main() {
Person* p = new Person(1);
cout << p->age << endl;
delete p; //结束Person对象,自动调用析构函数
}
十;申明和实现分离
在实际开发当中,我们尝尝做的是将申明与实现功能分来来处理。怎么一个分离呢?首先,我们用下面图片看看申明和实现具体是哪一部分。

那么,该如何操作呢?通常我们将申明部分放在一个后缀名“.h”当中,功能实现部分则放入另一份后缀名“.cpp”文件,在“.cpp”文件当中包含“.h”即可。具体该怎么操作呢?我们来看下面E.G
我们新建文件,“Class.h”文件来放入类的申明部分,“function.cpp”来放入类的功能实现部分:

10.1;定义类
我们在Class.h当中写入函数的申明,详细如下图:
#pragma once
class Person
{
int age;
public:
void getage(int age);
void potage();
};
10.2;定义成员函数功能
我们在function.cpp文件当中写入成员函数功能。这里我们需要将class.h文件包含进去。
#include <iostream>
#include "Class.h" //包含类定义文件
using namespace std;
void Person::getage(int age) //"Person::"表明该函数功能是属于哪个类的
{
this->age = age;
}
void Person::potage() {
cout << this->age << endl;
}
10.3;调用类
以上部分,我们就已经完成了一个类的申明和功能的分离,那么我们该如何去使用他们呢?答案是,在文件中包含文件头“Class.h”文件就可以了。详细如下
#include <iostream>
#include "Class.h" //包含定义类文件即可使用该文件当中的类创建对象
using namespace std;
int main() {
Person* p = new Person;
p->getage(3);
p->potage();
}
十一;命名空间
我们假设这样一个情况,如果我的程序当中出现两个类名相同的情况怎么办呢?当然了,有人会问了自己写的程序怎么会定义类名称相同呢?是啊,正常情况下是不会的,那如果我们是调用的其他人写的类呢?是不是不可避免的可能出现相同的情况了呢?面对这种情况,C++自然有对应的处理方案。“命名空间”的概念正是处理这个问题的。基本处理思想是在给相同的类名打上不同命名空间的标志,这样就可以通过不同的命名空间来区分相同类名。下面我们来通过代码来感受一下。
#include <iostream>
#include "Class.h"
using namespace std;
class student
{
public:
int age;
};
namespace B { //定义一个命名空间B,且将studnet类定义放入其中。
class student
{
public:
char name;
};
}
int main() {
student a; //默认是调用非自定义命令空间当中的类(默认调用全局命名空间的类)
a.age = 1;
cout << a.age << endl;
B::student b; //调用B命令空间当中定义的student类
b.name = 'a';
cout << b.name << endl;
}
十二;继承
继承的概念很简单,可以简单的概括为拿来主义。就是说如果我想用A类当中的函数,又不想直接调用A类。怎么办,继承它。那么如果去继承呢?在C++当中,使用“:”来继承。当然在继承下来,同样也是可以对A类当中的函数去修改。详细如下代码:
#include <stdio.h>
struct MyStruct
{
int age = 1;
int name = 2;
};
struct a :MyStruct //a继承于MyStruct
{
int b = 3;
};
int main() {
a* c = new a;
c->age;
c->name;
c->b;
return 0;
}
N;参考视频
小码哥教育,网名@M了个 J老师讲解视频《从汇编到C++入门到你想到游戏辅助编写》
未完待续