C++对象模型之绕过private权限修饰符

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、C++对象模型
  • 二、演示
    • 1.类层次
    • 2.内存排列
  • 总结


前言

咱们都知道C++语言在创建类的时候data member(数据成员)fuchtion member(函数成员),在访问权限上有3个access sections分别是privateprotectedpublic,我们都知道声明为private的成员只能在类内部被使用,但是这是一定的吗?其实这个策略上有一个漏洞,要了解这个漏洞你要理解C++的对象模型,这篇文章只是给你演示怎么理解对象模型,绝不是教你学坏这种方法在实际开发中不可取!


一、C++对象模型

C++对象模型是C++语言中描述如何创建、存储和操作对象的一套规则和结构。在C++中,对象是类的实例,而类定义了对象的属性(数据成员)和行为(成员函数)。C++的对象模型主要关注以下几个方面:

  1. 布局:每个对象在内存中都有一个特定的布局,这包括数据成员的顺序和对齐方式,以及虚函数表(如果有的话)的指针。C++标准并不精确规定对象的内存布局,但通常数据成员按照声明的顺序存放,同时考虑到处理器的对齐要求。

  2. 构造与析构:对象的生命周期始于构造函数的调用,结束于析构函数的执行。构造函数负责初始化对象的状态,而析构函数则负责清理对象占用的资源。在C++中,可以定义自己的构造函数和析构函数,也可以使用默认提供的。

  3. 继承与虚函数:C++支持继承,允许子类扩展或重写基类的行为。当一个类继承自另一个类时,子类对象会包含基类对象的完整状态。虚函数机制允许在派生类中重写基类的函数,实现多态性。

  4. 多态性:通过虚函数和抽象基类,C++支持运行时多态,即通过基类指针或引用来调用派生类的方法。这需要使用虚函数表(vtable)机制,每个含有虚函数的类都会有一个虚函数表,对象中包含指向这个表的指针。

  5. 内存管理:对象可以存储在不同的存储区中,包括栈、堆和静态存储区。栈上的对象在函数调用结束时自动销毁,堆上的对象需要手动管理其生命周期,而静态对象在程序整个运行期间都存在。

  6. 访问控制:C++中的类成员可以被声明为publicprotectedprivate,分别控制了类成员的可访问性。这有助于封装和数据隐藏,是面向对象编程的重要原则之一。

  7. 运算符重载:C++允许对基本运算符进行重载,使其适用于用户定义的类型。这可以使得自定义类型的对象像内置类型一样使用标准的运算符。

  8. 类型转换:C++支持多种类型转换,包括隐式转换、显式转换(通过static_castdynamic_castconst_castreinterpret_cast)以及用户定义的转换。

C++对象模型的复杂性在于它允许程序员在低级和高级抽象之间自由切换,从而能够编写高效且灵活的代码。然而,这也要求程序员对底层细节有深入的理解,以避免常见的陷阱和错误。

C++对象模型本身是非常复杂的概念,我们今天不去完全讲开,只是描述其中一个概念。

二、演示

下面的代码展示了一个类层次结构,所有的数据成员都是private的,理论上你是不能在类外部访问的,甚至派生类也不能直接访问。

1.类层次

C3.h

//
// Created by anold on 2024-07-22.
//#ifndef CLASS_C3_H
#define CLASS_C3_H#include <iostream>class C1 {
private:int val = 200;char c1 = '1';
};class C2 : public C1 {
private:char c2 = '2';
};class C3 : public C2 {
private:char c3 = '3';
};#endif //CLASS_C3_H

想像一下子这个C3类在内存里是怎么样存储的?想象一下C1、C2和C3对象各占多少字节的内存空间?

下面的代码会为你揭晓答案,这个地方我有必要贴出我的系统和编译器的信息,因为它确实在不同的环境下会展现出不一样的结果,但是今天我们只在我的环境下讨论。

OS:Windows 11 64bits
G++:GNU 13.1.0
GCC:GNU 13.1.0
IDE:CLion

#include <iostream>
#include "C3.h"int main() {C1 c1{};C2 c2{};C3 c3{};std::cout << sizeof(c1) << std::endl;std::cout << sizeof(c2) << std::endl;std::cout << sizeof(c3) << std::endl;return 0;
}

执行得到的3个对象都占8字节。

首先从C1看,C1作为基类没有虚函数,所以实际大小因该是int的4字节+char的1字节,但是由于要对齐,所以补上3字节共8字节。

C2也占8字节,那是为什么呢?那是因为C2=C1的subobject+char+对齐=int的4字节+char的1字节(C1)+char的1字节(C2)+补齐的2字节共8字节。

C3继承了C2,C3也占8字节那是为什么呢?那是因为C3=C2的subobject+int的4字节+char的1字节(C1)+char的1字节(C2)+char的1字节(C3)+补齐的1字节共8字节。

这下就清楚了吧,当然这种应该是最简单的对象模型,还有复杂一些的,比如带虚函数的,因为编译器会生成虚指针指向虚表,虚指针是编译器生成的插入对象的,本身也占空间。还有更复杂的多继承+虚继承内对象型更复杂,以后有机会再说,现在说说怎么绕过private。

2.内存排列

至少在我的环境下,我的这个代码中内存排列是确定的,请看下图:
在这里插入图片描述

所以类数据成员的排列是int+char+char+char,在C++的类对象模型中,数据成员的内存地址就是类的内存地址+偏移量(offset)。在这个范例里int的偏移量是0,也就是C3的地址;由于int在64位系统中占4字节,所以char的偏移量分别是4,5,6个字节。

虽然我们在IDE或编译器里面尝试从外部读取private修饰的变量会报错,但是有一种借助偏移量的方法可以间接读到,绕过这种安全机制。请看下面的代码:

	auto val = std::addressof(c3);auto ch_1 = reinterpret_cast<long long>(std::addressof(c3)) + 4;auto ch_2 = reinterpret_cast<long long>(std::addressof(c3)) + 5;auto ch_3 = reinterpret_cast<long long>(std::addressof(c3)) + 6;auto ch_1_c3 = reinterpret_cast<C3 *>(ch_1);auto ch_2_c3 = reinterpret_cast<C3 *>(ch_2);auto ch_3_c3 = reinterpret_cast<C3 *>(ch_3);printf("%d\n", *val);printf("%c\n", *ch_1_c3);printf("%c\n", *ch_2_c3);printf("%c\n", *ch_3_c3);

为什么要用reinterpret_cast?因为用std::addressof得到的类型是C3*,而C3*+4实际上偏移了4*sizeof(C3)个字节,也就是32字节,这显然是不对的。reinterpret_cast转换的结果和源对象拥有同样的位模式,这一点我有一篇文章单独讲透reinterpret_cast的,感兴趣的可以去看下。前提是目的类型和源类型必须至少等长,要不然会出现截断的问题。比如:转换成int就会转不回来了,这一点要特别注意!

我写的方法或许不是唯一的方法,但是原理是一样的,要准确找出偏移量

结果:

8
8
8
200
1
2
3

正好对应了类里面的几个数据。

理论上private修饰的元素是不能在类外部直接使用的,IDE不允许,编译器也不允许这么做。我们只是取巧绕过了这个机制罢了,本身有很大的风险,一不小心可能就超出边界了,而且这种布局不一定是一成不变的,会随着数据成员的改变而改变,所以不能用来正常开发


总结

1、C++的对象模型是相对复杂的概念,如果你想了解原理又绕不过去。
2、再一次重申,这种取巧的方法是非法的,虽然看起来有点意思,但是不能用来开发,它看起来特别像是一种设计缺陷,实际不过是一种取舍罢了,还是要靠程序员自己纠正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/48926.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux操作系统的有关常用的命令

1.linux系统的概述 1.1 什么是Linux系统? Linux&#xff0c;全称GNU/Linux&#xff0c;是一种免费使用和自由传播的类UNIX操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦 兹&#xff08;Linus Benedict Torvalds&#xff09;于1991年10月5日首次发布&#xff0c;它主要受…

LVGL项目实战之UI规划

LVGL项目实战之UI规划 ** 实物购买&#xff1a;TB 南山府嵌入式 ** 我们在在做项目之前&#xff0c;先需要确定项目的需求以及可能实现的功能&#xff0c;我们只有确定这些才能够对整体的框架进行把握。 本小结就说一下我们这个项目的一个整体的框架结构以及功能。 1-硬件构…

C语言实现二叉树以及二叉树的详细介绍

目录 1.树概念及结构 1.1树的概念 1.2树的相关概念 1.3树的表示 2.二叉树概念及结构 2.1二叉树的概念 2.2特殊的二叉树 2.3二叉树的性质 2.4二叉树的存储结构 3.二叉树顺序结构--特殊的二叉树--堆及其实现 3.1堆的概念及结构 3.2堆的实现 3.2.1堆的结构 3.2.2堆…

《JavaSE》---21.<简单认识Java的集合框架包装类泛型>

目录 前言 一、什么是集合框架 1.1类和接口总览 二、集合框架的重要性 2.1 开发中的使用 2.2 笔试及面试题 三、背后所涉及的数据结构 3.1 什么是数据结构 3.2 容器背后对应的数据结构 四、包装类 4.1 基本数据类型和对应的包装类 4.2 装箱和拆箱 1.最初的写法 2.…

org.springframework.context.ApplicationContext发送消息

1、创建消息的实体类 package com.demo;/*** 监听的实体类**/ public class EventMessage {private String name;public EventMessage(String name) {this.name name;}public String getName() {return name;}public void setName(String name) {this.name name;} }2、创建消…

【Linux】如何使用docker快速部署Stirling-PDF并实现远程处理本地文档

文章目录 前言1. 安装Docker2. 本地安装部署StirlingPDF3. Stirling-PDF功能介绍4. 安装cpolar内网穿透5. 固定Stirling-PDF公网地址 前言 本篇文章我们将在Linux上使用Docker在本地部署一个开源的PDF工具——Stirling PDF&#xff0c;并且结合cpolar的内网穿透实现公网随时随…

Java 集合框架:Java 中的双端队列 ArrayDeque 的实现

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 019 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

共享模型之无锁

一、问题提出 1.1 需求描述 有如下的需求&#xff0c;需要保证 account.withdraw() 取款方法的线程安全&#xff0c;代码如下&#xff1a; interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程&#xf…

GraphPad prism处理cck-8获得ic50

C组为空白对照组&#xff0c;a组为dmso对照组&#xff0c;b组为细胞加药组&#xff0c;八个梯度的药物浓度 一、数据转化 首先&#xff0c;打开软件&#xff0c;选项中选择x的第一项&#xff0c;y的第二项&#xff0c;单一药物浓度设定了几个孔就选几 把自己的药物浓度直接复制…

ubuntu22安装拼音输入法

专栏总目录 一、安装命令&#xff1a; sudo apt update sudo apt install fcitx sudo apt install fcitx-pinyin 二、切换输入法

游戏常用运行库安装包 Game Runtime Libraries Package

游戏常用运行库安装包&#xff08;Game Runtime Libraries Package&#xff09;是一个整合了多种游戏所需运行库的安装程序&#xff0c;旨在帮助玩家和开发者解决游戏无法正常运行的问题。该安装包支持从Windows XP到Windows 11的系统&#xff0c;并且具备自动检测系统并推荐合…

代码随想录训练第二十七天|LeetCode56.合并区间、LeetCode738.单调递增的数字、LeetCode968.监控二叉树

文章目录 56.合并区间思路 738.单调递增的数字思路 968.监控二叉树思路确定遍历顺序如何隔两个节点放一个摄像头 56.合并区间 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一…

Step-DPO 论文——数学大语言模型理解

论文题目&#xff1a;STEP-DPO: STEP-WISE PREFERENCE OPTIMIZATION FOR LONG-CHAIN REASONING OF LLMS 翻译为中文就是&#xff1a;“LLMs长链推理的逐步偏好优化” 论文由港中文贾佳亚团队推出&#xff0c;基于推理步骤的大模型优化策略&#xff0c;能够像老师教学生一样优…

String 和StringBuilder字符串操作快慢的举例比较

System.currentTimeMillis(); //当前时间与1970年1月1日午夜UTC之间的毫秒差。public class HelloWorld {public static void main(String[] args) {String s1 "";StringBuilder s2 new StringBuilder("");long time System.currentTimeMillis();long s…

git命令学习分享

分布式版本控制系统&#xff0c;本地仓库和远程仓库相互独立。 使用repository仓库进行控制&#xff0c;可以对里面的文件进行跟踪&#xff0c;复原。 git config --global --list&#xff1a;查看git配置列表 cd ** &#xff1a;进入** cd .. &#xff1a;退回上一级 echo…

AI Agent项目探索与实践记录

AI Agent项目探索与实践记录 1. 概述2. 总体结构2.1 记忆模块2.2 模型服务模块2.2.1 LLM服务2.2.2 retrieval服务2.2.3 rerank服务 2.3 Agent系统2.3.1 Planner2.3.2 Code/SQL Generator2.3.3 Code Executor2.3.4 Responser2.3.5 Round Compressor2.3.6 New Turn Discriminator…

基于Llama Index构建RAG应用(Datawhale AI 夏令营)

前言 Hello&#xff0c;大家好&#xff0c;我是GISer Liu&#x1f601;&#xff0c;一名热爱AI技术的GIS开发者&#xff0c;本文参与活动是2024 DataWhale AI夏令营&#xff1b;&#x1f632; 在本文中作者将通过&#xff1a; Gradio、Streamlit和LlamaIndex介绍 LlamaIndex 构…

全局 loading

好久不见&#xff01; 做项目中一直想用一个统一的 loading 状态控制全部的接口加载&#xff0c;但是一直不知道怎么处理&#xff0c;最近脑子突然灵光了一下想到了一个办法。 首先设置一个全局的 loading 状态&#xff0c;优先想到的就是 Pinia 然后因为页面会有很多接口会…

数据结构——栈(链式结构)

一、栈的链式存储结构 如果一个栈存在频繁进栈和出栈操作&#xff0c;可以考虑链式结构。 栈的链式存储结构是指使用链表来实现栈这种数据结构。在链式存储结构中&#xff0c;栈的每个元素被封装成一个节点&#xff0c;节点之间通过指针相连&#xff0c;形成一个链表。栈顶元…

Linux下开放指定端口

比如需要开放82端口&#xff1a; #查询是否开通 firewall-cmd --query-port82/tcp#开放端口82 firewall-cmd --zonepublic --add-port82/tcp --permanent#重新加载防火墙 firewall-cmd --reload