由底层和逻辑说开去——c++之类与对象的深入剖析
类是什么,对象是什么, 这两个问题在各个c++书里面都以一种抽象的描述方式,给了我们近乎完美的答案,然后我好像就知道什么是类什么是对象了,但是当扪心自问,类在哪儿,对象在哪儿,成员方法在哪儿,成员变量在哪儿的时候,这些定义大概只能给出一个同样抽象的答案。
其实很大程度上我们不知道问题的答案的原因是我们没有弄清楚我们的问题究竟是什么. 类和对象是拥有一堆有访问权限的成员变量和成员方法的集合,那么我们的问题就可以跟着这个凑合的定义得出,我当然也回答不了这些问题,但是我准备在本文做三件事情,通过这三件事,更加近的认识对象和类:1.从底层实现上讲,对象以什么形式保存,对象名是什么 对象的成员变量怎么存储 2.从底层上讲成员方法是怎么的一种存在,怎样把它和全局函数区分开,以及怎么做到重载 3.从逻辑层面上讲怎么实现访问权限,private ,const,static这些,以及从底层上如果绕过编译器去突破这些权限的限制,比如在static方法里成功访问对象成员变量,比如在const方法里面成功修改成员变量,比如在外部成功修改private变量;
然后我们就会发现,所谓c++的类机制,只是编译器在c的脖子上套上一把枷锁,又把钥匙交给了它。
第一个问题:类和对象的内存表示,我以前没学c++的时候,很多人就说啊,c++是面向对象的,c语言是面向过程的,让我有一种有对象就是面向对象的感觉,然后我就问面向对象是什么,然后人家就又说了,面向对象是一种思想,(然后抬头望向远方,做沉思状,让我有一种一巴掌踹死他的冲动);本文不打算说明面向对象是什么,因为这个思想的明白过程不是一蹴而就的,那就来说说内存中的实实在在的东西吧,毕竟存在的东西才踏实; 类是说给编译器听的,在内存没有任何的存在,就像结构体,就像数组,而对象才是是在存在的东西,对象名就像结构体变量名,就像数组变量名一样(有人说你扯淡吧,数组名是地址,你那两个东西算是什么东西),嗯
数组名是地址,在底层实现上,名字不都是地址吗,结构体变量名和对象名也是地址;这三个类型是复合类型,结构体和对象可以是不同类型的复合,数组是相同类型的复合,所以你可以在逻辑上使用数组名加1找到第二个元素的地址(但你要明白这都是逻辑上的,是编译器的功劳),但是结构体名加1却不一定;下面我们看一下一个对象创建过程是怎么分配内存的;源码如下:
#include <iostream>
using namespace std;
class TextA {
private:
int a;
int b;
public:
TextA(); };
TextA::TextA()
{
a=10;
b=20;
}
int main()
{
TextA text;
//cout<<sizeof(text);
return 0;
}
上面代码很简单,定义一个text(), 为它分配内存,让我们来看一下底层实现
.text:0040109F _main proc near ; CODE XREF: start+AFp
.text:0040109F
.text:0040109F _text = byte ptr -8
.text:0040109F argc = dword ptr 8
.text:0040109F argv = dword ptr 0Ch
.text:0040109F envp = dword ptr 10h
.text:0040109F
.text:0040109F push ebp
.text:004010A0 mov ebp, esp
.text:004010A2 sub esp, 8
;上面都不用看;
.text:004010A5 lea ecx, [ebp+_text] .text:004010A8 call _TextA.text:004010AD xor eax, eax.text:004010AF mov esp, ebp.text:004010B1 pop ebp.text:004010B2 retn.text:004010B2 _main endp.text:004010B2
这段代码其实重要的也就两句lea ecx,[ebp+_text]这句话大致意思是把text的地址放在ecx里面,然后
call _TextA就是调用TextA()默认构造函数 我来再看看,这个构造函数对text做了什么;
.text:0040107E _TextA proc near ; CODE XREF: _main+9p
.text:0040107E
.text:0040107E var_4 = dword ptr -4
.text:0040107E
.text:0040107E push ebp
.text:0040107F mov ebp, esp
.text:00401081 push ecx
.text:00401082 mov [ebp+var_4], ecx ;这句意思就是把ecx里面存的也就是_text标识的那块内存的地址放进ebp-4的内存;
.text:00401085 mov eax, [ebp+var_4] ;然后再放进eax里;
.text:00401088 mov dword ptr [eax], 0Ah ;0Ah就是十进制的10 把10放进eax存的地址的内存也就是_text标识的text的第一个变量a里面;
.text:0040108E mov ecx, [ebp+var_4] ;然后又一次把text标识的内存的地址放进ecx,
.text:00401091 mov dword ptr [ecx+4], 14h ;然后ecx里的地址减去四,得到的内存里面放 十六进制为14h也就是20的东西,显然这块内存是b;
.text:00401098 mov eax, [ebp+var_4]
.text:0040109B mov esp, ebp
.text:0040109D pop ebp
.text:0040109E retn
.text:0040109E _TextA endp
看吧 text还是标识它内存的首地址,也就是首元素a的地址,所以说从底层上讲结构题,数组和对象是一种东西; 在上面我们没有看到成成员方法啊,那成员方法在哪里呢?这就是我们的第二个问题了;
2.成员方法在哪里:这里面涉及一个命名粉碎机制,当然我也不懂命名粉碎原理
,但是大概就像是在编译的时候 根据你的函数的一些特征,给你的一个函数里面的代码段的段首取一个名字,嗯这句话至少包含三个层面的信息,第一,这个机制是编译的时候用的,可以让函数名变过去也可以变回来 第二函数名应用这个机制的时候取的特征由编译器决定,不同语言选择的不同,第三:得到的名字将用来标识原函数里面代码段的首地址,代码也是在内存里哦;
还是有点抽象哈,那么我们举几个例子: c 语言里面 只要函数名一样 不管参数类型一样不一样 都不能编译通过 这就说明这个特质是函数名,所以我们就说 c语言的函数名就是函数的地址; c++里面呢 有了重载就不能这样了,而且有了类成员函数,所以就不能这样了,c++里面的函数特征包括,所属类名,函数名,参数类型,参数多少等;当然也有一些没有所属类的方法也就是全局方法; c++的函数呢就放在代码段里面,用函数名(其实是变化后的来标识首地址);所以这在逻辑层上解释了几种现象 <1>在逻辑层上一个对象通过 .
操作符只能访问到它自己所属类的方法;<2> 成员方法其实是属于类的 跟对象没有关系(这句话说的不严谨,可能会引出一些问题,我们在第三个话题里讨论) <3>如果在一个成员方法里面定义个static类型变量,另一个对象使用该方法时,这个静态变量依然在;
比如下面的代码
#include <iostream>
using namespace std;
class TextA {
public:
void show()
{
static int a=1;
cout<<++a<<endl;
}
};
int main()
{
TextA ta;
TextA tb;
ta.show();
tb.show();
return 0;
}
输出2之后输出的是3,说明两者对象访问的是同一个地址的代码,也就是说这些成员方法属于类而不是对象本身,这就引出几个问题了,比如static方法老师们说才是类方法啊 比如说成员方法修改对象变量的时候怎么办,this指针又是什么东西;嗯这些问题我们就不留给第三个话题了,就在这里分析分析;首先我们来看一个成员方法的调用过程
#include <iostream>
using namespace std;
class Text
{
private:
int a;
public:
void set_a()
{
a=10;
};
};
int main()
{
Text t;
t.set_a();
return 0;
}
为了便于理解我们把代码写的很简单;简单到连参数都没有传,简单到没有默认构造函数;(放心编译器也不会给你加默认构造函数的,虽然老师和很多书上说一定会加,不信看下面汇编代码,原理我会在下一个博客解释 )我们看看这个代码的底层实现是怎样的;
_main proc near var_4= byte ptr -4
argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h push ebp
mov ebp, esp
push ecx
lea ecx, [ebp+var_4]
call ??1facet@locale@std@@UAE@XZ ; std::locale::facet::~facet(void)
xor eax, eax
mov esp, ebp
pop ebp
retn
_main endp
我们可以这到这个底层,只调用了一个函数
call ??1facet@locale@std@@UAE@XZ ; std::locale::facet::~facet(void)
??1facet@locale@std@@UAE@XZ就是名称粉碎后的结果,它标识了Text::set_a()的首地址;那么它是怎么得到this指针的呢,就是看
lea ecx, [ebp+var_4]这句话,这句话意思就是把t标识的地址放在寄存器ecx里面,也就是this指针,函数里面就可以用它找到a了,后面我们分析一下static方法就会发现它
没有有这句话 所以找不到this指针;
#include<iostream>
using namespace std;
class TextA {
private:
int a;
public:
static void show();
};
void TextA::show()
{
cout<<"dragonfive!";
}
int main()
{
TextA ta;
TextA::show();
return 0;
}
我们来看看底层实现
_main proc near argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h push ebp
mov ebp, esp
push ecx
call sub_40107E
xor eax, eax
mov esp, ebp
pop ebp
retn
_main endp
看吧这里就没有lea这句话,就不能得到this指针(这是编译器的做法,我们可以自己传一个,这样就能突破限制了这就是我们第三部分的内容了;)
3. c++里面有许多规定啊,显得莫名奇妙,比如private的成员不能在外界被访问,今天咱们就来访问一下试试:
#include<iostream>
using namespace std;
class TextA {
private:
int a;
public:
TextA(){a=10;}
void show_a(); };
void TextA::show_a()
{
cout<<a<<endl;;
}
int main()
{
TextA ta;
ta.show_a();
int *b=NULL;
__asm
{
lea eax,ta;
mov [b],eax;
}
*b=20;
ta.show_a();
return 0;
}
是吧,第一次输出的是10,因为初始化为10,然后第二次输出20,为什么呢,因为我们得到了a的地址嘛,那是不是说private是假的 自然不是了,因为private是c++的编译器的限制,我们用的是汇编把a的地址偷偷取到

,汇编自然不会走c++编译器也就不会受private限制,因此我们就知道了这个private啊 只是编译器的事情,跟变量的存储没有任何的关系;
当然由此可以推知其它的一些限制词也是这样子的,比如我们可以让static方法访问到访问它的对象的属性;
#include<iostream>
using namespace std;
class TextA {
private:
int a;
public:
TextA(){a=10;};
static void show();
};
void TextA::show()
{
int b;
__asm
{
mov eax,[ecx]
mov [b],eax
}
cout<<b;
}
int main()
{
TextA ta;
__asm
{
lea ecx,ta;
} TextA::show();
return 0;
}
lea ecx,ta;
只是因为我们在调用之前手动传递了一个地址进去额
所以如你所见 在底层实现上c++和其它语言没有什么区别 指针依然是那么强大而危险的存在着;
只是编译器通过对一些限制词的检测来保证一部分安全;为什么不能绝对安全,因为上一个博客里已经说了
c++的妥协性,指针的存在,让一切都只能把握在一个度内;使用c++便是为了通过这些限制词 让编译器尽可以地检测出不安全因素,
所以c++的函数是一种限制了的语言,对象就像皇宫,私有成员像是后宫部分...编译器就是把门的,只有拥有了指针也就是地址才能潜入后宫做各种友好访问。。。
最后发现好像说跑题了,因为说的貌似是指针的强大(这个可能会误导初学者),和限制词只是逻辑层的东西
由底层和逻辑说开去——c++之类与对象的深入剖析的更多相关文章
- 由底层和逻辑说开去--c++之引用的深入剖析
在学c++的时候 我遇到的第一个问题就是这个引用,引用是什么东西,我的c++启蒙教科书是c++ primer plus,这本书上说的是:引用是已定义变量的别名,可以使用这个引用来表示这个变量:每当看到 ...
- [Objective-C] 从NSInteger说开去
在iOS开发过程中,我一直习惯于使用C语法里的基本类型,而很少用(除非必须使用)Foundation的数据类型.最近看了一些资料,发现自己这样写可能有风险,虽然目前没遇到过相关的问题,但这是非常需要注 ...
- (转)2019年 React 新手学习指南 – 从 React 学习线路图说开去
原文:https://www.html.cn/archives/10111 注:本文根据 React 开发者学习线路图(2018) 结构编写了很多新手如何学习 React 的建议.2019 年有标题党 ...
- JSP 生命周期 理解JSP底层功能的关键就是去理解它们所遵守的生命周期
JSP 生命周期 理解JSP底层功能的关键就是去理解它们所遵守的生命周期. JSP生命周期就是从创建到销毁的整个过程,类似于servlet生命周期,区别在于JSP生命周期还包括将JSP文件编译成ser ...
- 从Linux内核升级的必要性说开去
Linux内核更新超级频繁,可是有必要时刻升级吗?个人感觉没有必要,可是你要时刻关注新特性列表,然后把自己的内核升级到离最新版本号差一两个月公布的版本号而不是最新版本号.以保证稳定性,由于一两个月的时 ...
- ArcGIS Engine开发之旅09--几何对象和空间参考
原文:ArcGIS Engine开发之旅09--几何对象和空间参考 1.Geometry Geometry 是 GIS 中使用最为广泛的对象集之一,用户在创建.删除.编辑和进行地理分析的时候,就是处 ...
- 从《BLAME!》说开去——新一代生产级卡通真实感混合的渲染方案
<BLAME!>是Polygon Pictures Inc.(以下简称PPI)创业33周年以来制作的第一部CG剧场电影,故事来自于贰瓶勉的同名漫画作品(中文译名为<探索者>或者 ...
- 由SOAP说开去 - - 谈谈WebServices、RMI、RPC、SOA、REST、XML、JSON
引子: 关于SOAP其实我一直模模糊糊不太理解,这种模模糊糊的感觉表述起来是这样: 在使用web服务时(功能接口),本来我就可以通过安卓中固有的http类(使用http协议),来发送http请求,并且 ...
- 从一个Bug说开去--解决问题的思路,Linked Server, Bulk Insert, DataTable 作为参数传递
声名— 部分内容为杜撰,如有雷同,不胜荣幸! 版权所有,如要引用,请标明出处! 如果打赏,请自便! 1 背景介绍 最近一周在忙一个SQL Server 的Bug,一个简单的Bug,更新两张 ...
随机推荐
- css笔记08:id选择器之父子选择器
1.父子选择器 (1)01.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" " ...
- Umbraco入门(一)--在VS中安装Umbraco
在VS中安装Umbraco 由于Vs中自己集成的IIS,所以在安装Umbraco是不用再想以前那么麻烦,需要设置IIS等等…… 使用VS的NuGet程序包管理器 创建一个用空的ASP Web应用程 ...
- Objective-C ,ios,iphone开发基础:多个视图(view)之间的切换,以及视图之间传值。
所有的视图都继承自 UIViewController,都使用 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nib ...
- Java之MS SQL数据库连接
一 1.首先,到微软官方下载jdbc驱动包 Microsoft JDBC Driver 4.0 for SQL Server 2.运行sqljdbc_4.0.2206.100_chs.exe,把文件 ...
- [改善Java代码]用整数类型处理货币
建议22:用整数类型处理货币. public class Client { public static void main(String[] args) { System.out.println(&q ...
- ListView使用自定义适配器的情况下实现适配器的控件点击事件执行Activity界面中的方法
如果ListView使用的是自定义的适配器,比如MyArrayAdapter extends ArrayAdapter<String> 那么,如何实现适配器中的点击事件执行activity ...
- img与父元素的间隙解决
近来在做H5页面时,突然发现一个问题,使用一个div包裹一个img,在手机预览时,发现图片与div之间有间隙. 当时第一反应就是,是不是间距没有设置为0,于是预览了下代码: .active img { ...
- less-1
说在前面的话,为什么用less: 1.需要编写的代码明显变少了 2.css管理更加容易 3.less学习成本低 4.使用less实现配色变得非常容易 5.兼容css3,实现各个浏览器中css的兼容写法 ...
- javascript+dom 做javascript图片库
废话不多说 直接贴代码 <!DOCTYPE html><html lang="en"><head> <meta charset=" ...
- 关于Class.forName("oracle.jdbc.driver.OracleDriver");报ClassNotFoundException 的异常
关于try { Class.forName("oracle.jdbc.driver.OracleDriver"); }catch(ClassNotFoundException e) ...