《网站建设》项目实训报告,广州seo优化排名推广,免费做网站怎么做网站吗2,淄博搜索引擎优化目录
1.初识面向对象
2.类的引入
3.类的定义
4.成员变量的命名规则
5.类的实例化
6.类对象模型
7.this指针
1.初识面向对象
C语言是一门面向过程的语言#xff0c;它关注的是完成任务所需要的过程#xff1b;C是一门面向对象的语言#xff0c;将一个任务分为多个对…目录
1.初识面向对象
2.类的引入
3.类的定义
4.成员变量的命名规则
5.类的实例化
6.类对象模型
7.this指针
1.初识面向对象
C语言是一门面向过程的语言它关注的是完成任务所需要的过程C是一门面向对象的语言将一个任务分为多个对象每个对象具有不同的行为将这些对象组合起来、互相交互从而达到完成任务的目的。
例如洗衣服这件事我们的惯性思维是将衣服放进洗衣机、撒上一些洗衣粉、放水、启动洗衣机、洗衣机工作完成将衣服拿出来晒......这就是一种典型的面向过程的思想所以说C语言更加符合人的思维逻辑。而面向对象的思维逻辑则不同它是一种宏观的、通用的思维逻辑还拿洗衣服这件事来说面向对象会将洗衣服这个任务拆分出两个对象即人和洗衣机人的行为有把衣服放进洗衣机、撒上洗衣粉、启动洗衣机、晒衣服等等洗衣机的行为有洗衣服、脱水等等那么这些对象之间通过互相交互就能完成洗衣服的任务。那么面向过程的思维导图可能是下面这样的 而面向对象的思维导图可能是这样的 2.类的引入
凭借C语言的经验我们可以知道结构体是某一类事务的属性集合。就比如用结构体描述一个学生我们可以发现学生当中的很多共性学生都有姓名、学号、年纪、班级......我们把这些属性集合起来构成一个结构体再用这个结构体去定义一个变量这个变量就可以说是一个具体的人。那么C语言的局限性非常明显结构体不能定义行为(函数)虽然可以定义函数指针但是这种做法好像没必要。所以C便出手了在原有的struct关键字上进行了扩展其中之一便是struct结构体中不仅可以定义变量还可以定义函数并且C的struct关键字保留了C语言的所有用法。所以我们再对学生的例子进行扩展我们把学生的共有属性集合到一起之后我们还可以发掘他们之间的共有行为学生都会上课、吃饭、睡觉......那么用C描述学生就非常合适了
struct Student
{char* name;char* id;int age;void eat(){} // 吃饭行为void sleep(){}// 睡觉行为void study(){}// 学习行为
};
在C中就没有结构体的概念了我们上面代码的Student称为Student类这个Student是一个自定义类型与内置类型不同的是内置类型是C帮我们定义好的类型我们开箱即用而自定义类型是我们自己规定的一个类型如何管理这个类型是程序员自己要做的事。举个例子内置类型int可以支持、-、*、/等运算如果我们想要Student类也支持这些运算是我们程序员自己要去规定的(在后面的章节会介绍有关这部分的内容)。
那么类型有了我们该如何定义变量注意此时我将变量这两个字用双引号括起来目的就是为了告诉读者在C中几乎不会出现变量这种说法而是统称对象。我们重新问一遍有了Student类如何定义Student类型的对象
int main()
{// 哪种定义方式是正确的struct Student s1;// 写法1Student s2;// 写法2return 0;
}
事实上写法1在C的角度看来是错误的写法C更倾向于写法2也就是说在C中使用struct关键字实现的类在实例化对象时不需要再增加struct关键字注意这里又有一个新名词——实例化实际上实例化和定义没有区别这只是C的习惯。因为C要兼容C语言所以保留了看似累赘的写法。
3.类的定义
定义类的时候我们将在类中定义的变量称为成员变量定义的函数称为成员函数或者成员方法这些成员函数都被编译器视为内联函数(即使我们不加inline关键字)。同时类的定义构建了一个全新的作用域——类域。
既然类构成了类域在类中使用成员时是否也需要遵循编译器的向上搜索呢
struct Student
{char* name;// 函数当中这样使用成员是否正确void Init(){name ;id 0;age 0;}char* id;int age;
};
这种使用方法是没有问题的也就是说类域与常规的作用域不一样。编译器将类域当成一个整体当某个成员函数需要使用某个成员变量时会在整个类域当中搜索。
如果我们在类中声明函数而在类外定义函数就需要使用类域来配合完成工作
#include stdlib.h
struct Stack
{void Init();int* a;int top;int capacity;
};struct Queue
{void Init();int* a;int front;int tail;int capacity;
};void Init()
{a (int*)malloc(4);top 0;capacity 4;
}void Init()
{a (int*)malloc(4);front 0;tail 0;capacity 4;
}
这种写法是错误的。一是函数重定义二是在类外定义的函数是普通函数这些函数的作用域使用了当前函数作用域和全局域都没有定义的对象。我们的本意是定义Stack类和Queue类当中声明的Init函数所以我们需要使用类域指明到底是哪个类的成员函数。当我们指明了成员函数后编译器就知道了该函数不是全局函数而是某一个类当中的成员函数所以自然而然能够使用类中其他的成员
void Stack::Init()
{a (int*)malloc(4);top 0;capacity 4;
}void Queue::Init()
{a (int*)malloc(4);front 0;tail 0;capacity 4;
}
那么要在类外使用成员的时候需要通过对象去使用不能直接使用类域指定成员使用这样的语法是错误的
struct Stack
{void Init(){a (int*)malloc(4);top capacity 0;}int* a;int top;int capacity;
};int main()
{// 正确的用法Stack st;st.Init();cout st.top endl;// 错误的用法Stack::Init();cout Stack::top endl;return 0;
}
其实在C当中并不喜欢用struct定义类而是用一个新的关键字——class来定义类。我们可以用class替换struct但我们很可能会因为不熟悉而触发bug
using namespace std;
#include stdlib.hclass Stack
{void Init(){a (int*)malloc(4);top capacity 0;}int* a;int top;int capacity;
};int main()
{Stack st;st.Init();cout st.top endl;return 0;
}
如果大家勤快的话将这段代码粘贴到你的编译器当中会发现你的编译器报错了。 这就不得不提到面向对象的三大特性封装、继承、多态。三大特性并不是只面向对象只有这三个特性而是这三个特性在面向对象中占据主要地位。那么C为了考虑封装性引入了访问限定符public(公有)、protected(保护)、private(私有)而现在我们主要使用两个访问限定符即public和private。
访问限定符说明 1.public修饰的成员可以在类外直接访问(通过对象) 2.private修饰的成员在类外不能直接访问 3.访问权限的作用域从当前访问限定符的位置直到类域结束或者直到下一个访问限定符 4.class定义的类的访问权限默认为privatestruct默认为public
我们修改上面的代码我们的本意是让Stack类的成员变量不能在类外访问在类外能访问的只能是成员函数。这种屏蔽底层实现而只暴露接口的做法是封装的常用手段。
class Stack
{
public:void Init(){a (int*)malloc(4);top capacity 0;}
private:int* a;int top;int capacity;
};
我们能够推导出class和struct的区别就在于默认的访问权限不同。
4.成员变量的命名规则
上面的成员变量的命名都是不规范的我们观察一下代码
class Date// 日期类
{
public:void Init(int year 0, int month 0, int day 0){year year;month month;day day;}
private:int year;int month;int day;
};
有的读者可能就会钻牛角尖了修改一下Init函数的参数不就可以了吗道理是这么个道理实际上真正的工程项目可能有几十几百个类如果把成员函数的参数都修改成a、b、c......那还得了所以在设计类的时候就需要考虑这个问题C的习惯(实际上不是C规定的而是公司、企业里面规定的)是在定义成员变量时在其变量名之前或者之后加一个或多个_从而区分成员变量和非成员变量
class Date// 日期类
{
public:void Init(int year 0, int month 0, int day 0){_year year;_month month;_day day;}
private:int _year;int _month;int _day;
};
5.类的实例化
使用类类型创建对象的过程就称为类的实例化。
对于类我们应当有一下两点认识 1.类是对对象进行描述的是一个抽象的类型。定义类的时候并不会给它分配实际的内存空间来存储它 2.一个类可以实例化出多个对象实例化出的对象才实际占用物理空间存储类的成员(暂且这么理解)
以一个具体的例子来方便大家理解我们可以把类看成建筑物的设计图纸而对象便是通过这张图纸实例化出的建筑物。
我们在上面说过不能直接通过类域去访问类当中的成员其中一个原因便是因为不能去操作设计图纸
#include iostream
using namespace std;
class Date// 日期类
{
public:void Init(int year 0, int month 0, int day 0){_year year;_month month;_day day;}void print(){cout _year : _month : _day endl;}
private:int _year;int _month;int _day;
};int main()
{Date::_year 2023;// 错误的用法Date::print();// 也是错误的return 0;
}
对于成员变量来说它们在实例化对象之前都没有实际的存储空间没有实际空间意味着它们只是声明。而我们要将整数2023赋值给一个没有空间的成员变量本身就是一种错误的做法。拿具体的例子来说售楼中心有会有一个沙盘模型它可以详细的看到每一栋楼、每一间房的具体设计我们就可以把这个沙盘模型看作一个类某天我们购买了一个冰箱能直接放进沙盘模型里面去吗当然也有读者会产生一个疑问那么成员函数print已经是定义好的函数为什么不能指定类域直接访问确实print函数已经被定义好了但是它缺失了一个调用条件这个调用条件便是后面要介绍的this指针。
6.类对象模型
先计算下面这段程序中A类对象的大小(代码是在x86环境下跑的计算过程与计算结构体大小相同)
#include iostream
using namespace std;
class A
{
public:void print(){cout _a endl;}
private:int _a;
};int main()
{A a;cout sizeof(a) endl;return 0;
} 很多读者觉得输出的结果应该是8因为类当中有一个函数可以当成函数指针来看待。实际上输出的是4也就是说并没有计算函数的大小换句话说成员函数好像并不在对象中存储。我们可以猜测三种类对象的存储方式 1.对象中存储类中声明的每个成员 这种做法的缺陷是很明显的会浪费很多空间。我们确实需要保证对象与对象之间的成员变量是独立的但是成员函数并不需要各自私有一份因为每个对象的行为都是一样的每个对象调用的成员函数都是同一个函数。 2.成员函数放在内存的某个区域对象模型只存储一个指向该区域的指针 很显然这种方案即使再合理也不被C采用(已经证明过了)。 3.只保存成员变量成员函数存放在代码段当中 很明显C采用第三种对象存储模型。我们可以这么推理这些成员函数都属于特定的类那么编译器在维护这些函数时一定有方法可以分辨对象在调用成员函数时一定只能调用属于该类域的函数编译器就能够通过对象调用的函数去找到属于该类域的函数。
我们再研究一个不寻常的问题请读者猜一猜该段程序当中A、B、C类的大小各是多少
#include iostream
using namespace std;
class A
{};class B
{
public:void print(){}
};class C
{
public:void print(){}
private:int _c;
};int main()
{cout sizeof(A) endl;cout sizeof(B) endl;cout sizeof(C) endl;return 0;
} 那么对于类A和类B我们都可以把它看作空类(对象模型不存储成员函数)其大小为1类C的大小毋庸置疑为4。那么为什么空类的大小为1这是一个占位大小目的是告诉编译器存在这个类这个类还能实例化出对象如果空类的大小为0那么对象的地址是取不到的
int main()
{A a;B b;cout a endl;cout b endl;return 0;
}
空类的大小设置为1表明该类是一个有效类、合法类可以实例化出对象并且该对象在内存当中是持有内存空间的否则无法对其进行取地址操作。
7.this指针
以日期类为例
#include iostream
using namespace std;
class Date// 日期类
{
public:void Init(int year 0, int month 0, int day 0){_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(2023, 4, 22);d2.Init(2024, 12, 11);d1.print();d2.print();return 0;
} 为什么d1和d2对象调用一个函数却能够打印出不同的结果原因在于C为类当中的每个普通成员函数增加了一个隐藏的this指针注意我说的普通成员函数后面还会介绍静态成员函数。这个this指针会被当做普通成员函数的第一个参数this指针是指向调用该普通成员函数的对象在普通成员函数的内部需要使用成员变量的场景编译器都会隐式地发生解引用。this指针的原型为[类名* const this],其中this以关键字的形式存在不可被修改。
那么上面的Date类当中的print成员函数实际上的形式这样的
void print(Date* const this)// 实际的代码当中不能这么写
{// 这种写法是可以的cout this-_year : this-_month : this-_day endl;
}
虽然说上面这段才是print函数全貌但我们自己在写的时候不能显式定义this指针但我们可以显式地使用this指针。
那么this指针从何而来实际上在外部使用对象调用这些普通成员函数的时候编译器就会自动地、隐式地将调用普通成员函数的对象的地址(指针)作为实参传递过去
int main()
{Date d1, d2;d1.Init(2023, 4, 22);// 隐式传递d1对象的地址d2.Init(2024, 12, 11);// 隐式传递d2对象的地址d1.print();// 隐式传递d1对象的地址d2.print();// 隐式传递d2对象的地址return 0;
}
那么现在又有一个关键问题this指针能否为空指针(一道面试题)答案是可以我们看下面的两段代码
#include iostream
using namespace std;class A
{
public:void print(){cout print() endl;}
private:int _a;
};int main()
{A* pa nullptr;pa-print();return 0;
}
这段代码是编译错误还是运行时崩溃或者是正常运行答案是正常运行其原因在于pa指针即使是一个空的对象指针即使使用了-但是它并不会发生解引用而是指明类域之后编译器就能找到正确的函数调用因为对象的成员函数并不存储在对象当中而是存储在了所有同类对象可见的代码段。调用print函数时编译器会隐式的传递一个对象指针现在已经有了一个现成的对象指针就是papa传递给print后在其内部并没有发生任何有关空指针的解引用问题所以该程序不会发生错误反而是正常运行。
#include iostream
using namespace std;class A
{
public:void print(){cout _a endl;}
private:int _a;
};int main()
{A* pa nullptr;pa-print();return 0;
}
这段程序非常执行结果非常明显运行时崩溃原因是发生了空指针的解引用。
最后再解答一个遗留的问题为什么不能在类外通过使用类域指定调用普通成员函数
#include iostream
using namespace std;class A
{
public:void print(){cout _a endl;}
private:int _a;
};int main()
{A::print();return 0;
}
事实上答案已经非常简单了。print作为A类的普通成员函数表面上看起来是一个无参的函数但实际上它有一个隐藏的this指针所以print是一个单参数的函数。而在外部使用类域指定调用print时指定的是无参的print而A类当中并没有无参的prnit函数所以不能调用。在不改变A类内部结构的情况下我们无法修改代码使得通过编译因为C不允许我们显式传递this指针。
那么this指针变量存储在哪里很多读者会认为this指针作为成员函数的参数应该存储在代码段中实际上this指针作为成员函数的形参只有在函数被调用时才会创建函数栈创建了函数栈才能存放this指针所以this指针存储在栈中。