惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
腾讯CDC
V
V2EX
Martin Fowler
Martin Fowler
A
About on SuperTechFans
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Check Point Blog
博客园 - 【当耐特】
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
K
Kaspersky official blog
Security Latest
Security Latest
H
Help Net Security
博客园_首页
美团技术团队
Spread Privacy
Spread Privacy
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
S
SegmentFault 最新的问题
G
Google Developers Blog
NISL@THU
NISL@THU
爱范儿
爱范儿
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security @ Cisco Blogs
Schneier on Security
Schneier on Security
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
V
Vulnerabilities – Threatpost
W
WeLiveSecurity
P
Palo Alto Networks Blog
G
GRAHAM CLULEY
Hacker News: Ask HN
Hacker News: Ask HN
I
InfoQ
The Cloudflare Blog
F
Full Disclosure
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium

博客园 - GerJCS

《编程指北》动手写 C++ shared_ptr 章节开始(没钱买书,靠无尽追问豆包,反复思考质疑,一路拓展出很多东西,包括手写了个内存池,因为始终感觉没什么东西都是枯燥的概念对比,用Linux实践malloc又看不到RES回落,于是不知不觉摸索到了内存池) 《编程指北》动手写 C++ shared_ptr 章节开始(没钱买书,靠无尽追问豆包,反复思考质疑,一路拓展出很多东西,包括手写了个内存池,因为始终感觉没什么东西都是枯燥的概念对比,用Linux实践malloc又看不到RES回落,于是不知不觉摸索到了内存池) 续啃《编程指北 C++》智能指针(牵扯无穷无尽的其他知识) 续啃:编程指北 C++ (从 RAII开始的,RAII这一小节,学了将近两个月,RAII 的内容早在之前就主动追问豆包搞懂了,这节主要是自己无尽追问探索出很多其他知识,后来发现其实堪比精啃 CSAPP & APUE 等圣书) 续:啃操作系统 项目:从零开始做一个HTTP服务器(准备篇 —— 无尽弯路错路) C++:继续上一篇文章学CGI编程:乱码问题、误入歧途学了CGI编程(其实只是安装劝退因祸得福)、通过知乎直答及时更改学习方向、找到方向(花了2天) C++:网页网站、互联网、服务器、(web)服务器、(http)服务器、超文本、大型数据中心、云服务器、访问网页发生的流程、URL、DNS、域名、网址链接、IP地址、路由器、CGI架构图、万维网、tomcat、servlet、apache、学C++意义、动态静态请求、脚本、编码、Editplus、乱码 C++:多线程、进程、std::thread、互斥锁、reference_wrapper()、lambda()、cv.wait()、ref、条件变量、原子操作 、看书的意义(内存管理seq和宽松这需要看看)、线程局部存储、死锁、线程间通信(future/promise)、execution库并行 C++:#define等宏预处理器、中断信号处理 C++:命名空间、模版 C++:异常处理、二维数组/三维数组/对象的动态内存分配与delete释放 C++:数据抽象、数据封装、接口(之前做测试的时候听他们说接口始终不理解)、文件和流 C++:基本之前都深入问过豆包了,没啥新东西:类和对象、get()set()、公有私有保护、作用域解析运算符::、继承、函数重载、多态(从这开始不帖回答了,只放豆包回答链接)、虚函数 C++:结构体、vector、队列、链表、哈希表、映射map、简述回顾O(logn)复杂度、哈希表和桶排序里的“桶”、set C++ 基本的输入输出、缓冲机制、cerr引发的超长折磨、同步问题 C++:数组、字符串、指针和引用、时间戳、覆盖静态存储区问题 C++:三角函数、随机数、数论线性同余(待研究)、math库函数、配置C++20、C++20的number、GCC版本和C++版本区别、M_PI、条件编译、头文件和命名空间和库函数区别、控制台/终端/命令行/cmd/bash lambda后续,实在受不了了,太痛苦了,全网找不到答案,详见最后,搜“诡异”,一个lambda整整搞了6天 菜鸟教程:运算符、指针和引用、(从刷算法题到现在目前为止最难啃的)Lambda、第一次主动了解学习new。对我来说这篇博客写的异常痛苦,不亚于刷过的最难算法题,最异常痛苦的是豆包的回答是错的,亏我还追着问了他整整3天关于lambda的事 菜鸟教程:存储类 菜鸟教程:修饰符、静态非静态、构造函数、类相关的杂七杂八的知识
编程指北的 C++
GerJCS · 2025-10-19 · via 博客园 - GerJCS

const 是只读

const int a = 10;
a = 20; // 编译错误,a 是只读变量,不能被修改

想去掉只读就用类型强制转换const_cast,但用不好会崩溃或未定义。

实际

const int a = 10;
const int* p = &a;
int* q = const_cast<int*>(p);
*q = 20;  // 通过指针间接修改 const 变量的值
std::cout << "a = " << a << std::endl;  // 输出 a 的值,结果为 10

先科普下硬编码:直接把值写死在代码里,而非从变量或内存中读取。

const变量,编译器可能优化成 “只写内存一次,之后直接用硬编码值”,之后不从内存读,所有用到 a 的地方都替换成字面量 10,即内存的对应位置确实改成 20了,但 cout << a 输出是硬编码没懂 10,不是内存实际值

既然通过指针改没用,那咋改?提供了const_cast,不是说会导致问题吗?那咋还用?

具体用法:

用法1、处理 “const 误标” 的接口

调用一个老旧库的函数,它参数要求 非 const char*,但你手里只有 const char*(比如字符串字面量)。此时用 const_cast 去掉 const,可以让代码兼容,前提是你确定函数不会真的修改内容(否则行为未定义)

void oldFunc(char* str) { /* 实际没修改 str */ }

int main() {
    const char* s = "hello";
    // oldFunc(s);  // 直接传会报错,因为类型不匹配
    oldFunc(const_cast<char*>(s));  // 合法兼容(假设函数真的不修改)
}

用法2、实现 “逻辑 const” 的成员函数

class Cache {
    mutable std::string cachedData;  // mutable 表示可在 const 函数修改,但这里用 const_cast 演示另一种思路
    bool cacheValid = false;

public:
    std::string getData() const {
        if (!cacheValid) {
            // 这里 this 是 const Cache*,需要去掉 const 才能修改成员
            const_cast<Cache*>(this)->cachedData = "real data";
            const_cast<Cache*>(this)->cacheValid = true;
        }
        return cachedData;
    }
};

开始解读:

mutable 是 C++ 关键字,为“特许改”,用于修饰类的成员变量,打破 const 成员函数的限制 —— 让被修饰的变量即使在 const 成员函数中也能被修改

const 为“限制改”

解读这个代码需要很多前设知识, 开始说前设知识:

前设知识 —— 类:

类的基本结构:

class 类名 {
    // 这里放“属性”(也叫“成员变量”)—— 描述这个类有什么数据
    数据类型 变量名1;
    数据类型 变量名2;

public:  // “public”表示后面的内容是“公开的”,外面可以直接用
    // 这里放“功能”(也叫“成员函数”)—— 描述这个类能做什么
    返回类型 函数名1() {
        // 函数里的具体操作
    }
    返回类型 函数名2() {
        // 函数里的具体操作
    }
};

一个简单的学生类

class Student {
    // 属性(成员变量):学生的名字、年龄(这些是“内部数据”)
    string name;  // 名字
    int age;      // 年龄

public:  // 公开的功能
    // 功能1:设置学生的信息
    void setInfo(string n, int a) {
        name = n;  // 给名字赋值
        age = a;   // 给年龄赋值
    }

    // 功能2:打印学生的信息
    void printInfo() {
        cout << "名字:" << name << ",年龄:" << age << endl;
    }
};

咋用这个类?

类只是 “模板”,必须根据模板造出 “实物” 才能用,这个 “实物” 叫 “对象”,就像根据手机图纸造出真实的手机:

int main() {
    // 创建一个Student类的对象(实例),名字叫“stu”
    Student stu;

    // 用对象调用公开的功能(通过“.”符号)
    stu.setInfo("小明", 18);  // 调用setInfo功能,设置信息
    stu.printInfo();          // 调用printInfo功能,打印信息
}

输出::名字:小明,年龄:18

怎么理解代码里的符号?

  1. . 符号:当你有一个 “对象”(比如stu),想调用它的功能或访问属性时,用 对象.功能() 或 对象.属性(前提是属性是公开的)。
    例:stu.setInfo(...)stu.printInfo()

  2. -> 符号:如果手里不是 “对象” 本身,而是 “指向对象的指针”(可以理解为 “对象的地址”),就用 指针->功能() 或 指针->属性

    Student stu;          // 创建对象stu
    Student* p = &stu;    // p是“指向stu的指针”(存的是stu的地址)
    p->setInfo("小红", 17);  // 用指针调用功能,等价于 stu.setInfo(...)
    p->printInfo();
  3. this指针:在类的 “成员函数”(比如setInfoprintInfo)内部,有一个隐藏的指针叫this,它自动指向当前正在使用的对象。

比如调用 stu.setInfo(...) 时,setInfo 函数里的this就指向stu

调用 p->setInfo(...) 时,setInfo 函数里的this就指向p所指的对象(还是stu)。

所以在函数里可以用 this->属性 来明确表示 “当前对象的属性”,比如:

void setInfo(string n, int a) {
    this->name = n;  // 等价于直接写 name = n(因为this默认指向当前对象)
    this->age = a;   // 等价于直接写 age = a
}

(平时可以省略this->,但它确实存在)

C++ 为何牛逼?我也不知道,但问了豆包 C 该咋写?如下

C 写法:

查看代码
#include <stdio.h>
#include <string.h>
// 定义学生结构体(类似类的属性)
struct Student {
    char name[20];  // 名字
    int age;        // 年龄
};

// 模拟"设置信息"的功能(类似类的成员函数)
void setInfo(struct Student* stu, const char* name, int age) {
    strcpy(stu->name, name);  // 给名字赋值
    stu->age = age;           // 给年龄赋值
}

// 模拟"打印信息"的功能
void printInfo(const struct Student* stu) {
    printf("名字:%s,年龄:%d\n", stu->name, stu->age);
}

int main() {
    // 创建结构体变量(类似对象)
    struct Student stu;
    
    // 调用函数(通过结构体指针访问,类似"."操作符)
    setInfo(&stu, "小明", 18);  // 传入结构体地址
    printInfo(&stu);
}
  1. C 语言用struct定义结构体,没有public这类访问控制

  2. 没有成员函数,只能通过普通函数 + 结构体指针来操作数据

  3. ->符号通过指针访问结构体成员(替代 C++ 的.操作符)

  4. 必须显式传递结构体地址作为函数参数(C++ 的this指针是隐藏的)

至此科普完一些 前设知识 可以讲 用法2 那个代码了:

先忽略const相关的词:

class Cache {
    // 属性(成员变量)
    std::string cachedData;  // 缓存的数据
    bool cacheValid = false; // 缓存是否有效的标记(false=无效)

public:
    // 功能(成员函数):获取数据
    std::string getData() {  // 先去掉const,简化理解
        if (!cacheValid) {  // 如果缓存无效(需要更新)
            // 给当前对象的cachedData赋值
            this->cachedData = "real data";  // this指向当前Cache对象
            this->cacheValid = true;         // 标记缓存有效
        }
        return cachedData;  // 返回缓存的数据
    }
};
  1. Cache 是一个 “缓存类”,用来存数据(cachedData),并记录数据是否有效(cacheValid)。
  2. 调用 getData() 功能时,先检查缓存是否有效:
    • 如果无效(cacheValidfalse),就更新cachedData,并标记为有效(cacheValid = true)。
    • 如果有效,直接返回已有的cachedData(不用重复更新,节省时间)。

最后解释为什么提到mutableconst_cast

之前的代码里,getData() 后面有个constgetData() const),这个const表示 “这个函数承诺不修改类的属性”。

但实际场景中,我们需要在这个函数里修改cachedDatacacheValid(否则无法更新缓存),所以有两种解决办法:

  1. mutable修饰这两个属性(mutable std::string cachedData;),表示 “即使在const函数里也能修改它们”。

  2. const_cast临时去掉thisconst限制(const_cast<Cache*>(this)->cachedData = ...),强制允许修改  

因此有两种写法都可以:

查看代码
//写法一、
#include <iostream>
#include <string>

class Cache {
    std::string cachedData;
    bool cacheValid = false;

public:
    std::string getData() const {
        if (!cacheValid) {
            const_cast<Cache*>(this)->cachedData = "real data";
            const_cast<Cache*>(this)->cacheValid = true;
        }
        return cachedData;
    }
};


//写法二、
#include <iostream>
#include <string>

class Cache {
    mutable std::string cachedData;
    mutable bool cacheValid = false;

public:
    std::string getData() const {
        if (!cacheValid) {
            cachedData = "real data";
            cacheValid = true;
        }
        return cachedData;
    }
};

顺便记录下调教豆包的关键词,起初像挤牙膏一样很墨迹心累,后来用了这个自己创造的提示词,解释的透彻了

查看代码
if (!cacheValid) {  // 如果缓存无效(cacheValid是false)
    // 这里需要更新缓存
    const_cast<Cache*>(this)->cachedData = "real data";
    const_cast<Cache*>(this)->cacheValid = true;
}
return cachedData;  // 返回缓存的数据
这里什么this、→、我完全看不懂
我对C++完全0基础
另外这里说:mutable 表示可在 const 函数修改,但这里用 const_cast 演示另一种思路
啥意思 ??演示的到底是啥啊?语言表达这么歧义吗???不就是想做演示const吗?
咋又扯到mutable了?????

你解释东西先别解释这些const、const_cast啥的了!!!

我连最基基本的类public这些都不懂

从最基础讲起

至此把这个傻逼说的展开学习完毕

继续

const 修饰函数参数

void func(const int a) {
    // 编译错误,不能修改 a 的值
    a = 10;
}

安全,避免在函数内部无意中修改传入的参数值。

尤其是 引用 作为参数,void func(const int& a) { ... },如果确定不会修改引用,那么一定要使用 const 引用。& 是引用的标志,const int& a 表示 a 是 int 类型的 const 引用

继续

const 修饰函数

妈逼的感觉讲的跟垃圾菜鸟教程有一拼,艹,甚至都不如菜鸟教程,老子看你教程,就因为比菜鸟教程好在:紧贴时事不过时2025年的、紧紧围绕大厂不至于学偏。起初看他公众号我还以为多牛逼,但就这,就已经他妈是这行业里大佬了,呵呵

#include <iostream>
using namespace std;
const int func() {
    int a = 10;
    return a;
}

int main() {
    const int b = func(); // b 的值为 10,不能被修改
    // b = 20; // 编译错误,b 是只读变量,不能被修改
    cout<<b<<endl;
}

这里例子都他妈没举好

函数返回值的const作用的是函数返回的临时值本身,但这个临时值在赋值给变量时会发生拷贝,而拷贝后的变量是否可修改,由变量自己的类型决定。

代码里,

当你写 int b = func() 时,函数返回的const int是一个临时值(比如10),这个临时值确实是const的(不能被修改),但它会被拷贝给变量b。此时b的类型是int(非const),所以b可以被修改,没啥意义。

如果返回的是指针或引用const的意义就很大了。比如:

const int* func() { // 返回指向const int的指针
    static int a = 10;
    return &a;
}

int main() {
    int* p = func(); // 编译错误!因为func返回的是const int*,不能赋值给int*
    const int* p2 = func(); // 正确,p2不能通过指针修改a的值
    return 0;
}

const int* p2 = func() 中,const int* 修饰的是 指针 p2 所指向的内容,限制的是:不能通过 p2 这个指针来修改它指向的变量 a 的值。p2 指向的是 func() 里的 static int a(值为 10)。因为 p2 是 const int* 类型,所以像 *p2 = 20; 这样的代码会编译错误(禁止通过 p2 修改 a)。但 a 本身不是 const 变量,如果在 func() 内部或通过其他非 const 指针,仍然可以修改 a(比如在 func() 里加 a = 20;)。

但 p2 可以指向其他

简单说:const int* p2 的 const 是给指针 p2 加的 “限制”,让它不能当 “修改工具”,和变量 a 本身是否可改无关

看下面的评论真他妈纯浪费时间,都是一坨屎。洛谷 和 poj 的那才叫评论

我这么细心钻研,我一定要超过 鱼皮 和 编程指北

无意间读到的:这都啥水平啊?才发现吗?

继续

有了豆包的讲述,看他的才懂,直接复制过来,不用我多逼逼

const 修饰 指针 或 引用

1、指向只读变量的指针

这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身

因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量

const int* p;  // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a;  // 合法,指针可以指向普通变量
p = &b;  // 合法,指针可以指向只读变量
*p = 30;  // 非法,无法通过指针修改只读变量的值

我们可以将指针指向普通变量或者只读变量,但是无法通过指针修改只读变量的值。

2、只读指针

const 关键字修饰的是指针本身,使得指针本身成为只读变量。

因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。

int a = 10;
int b = 20;
int* const p = &a;  // 声明一个只读指针,指向 a
*p = 30;  // 合法,可以通过指针修改 a 的值
p = &b;  // 非法,无法修改只读指针的值

在上面的例子中,我们使用 int* const 声明了一个只读指针 p,指向变量 a。我们可以通过指针修改 a 的值,但是无法修改指针的值

3、只读指针指向只读变量

const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。

因此,指针本身不能被修改,也不能通过指针修改所指向的变量。

const int a = 10;
const int* const p = &a;  // 声明一个只读指针,指向只读变量 a
*p = 20;  // 非法,无法通过指针修改只读变量的值
p = nullptr;  // 非法,无法修改只读指针的值

4、常量引用

先科普:

在 C++ 中,& 有两种常见含义,需要根据语境区分:

  1. 当 & 用于变量声明时(如 int& b,它表示 “引用”,不是地址。

    • const int& b = a 中,b 是 a 的常量引用,本质是 a 的 “别名”,指向原数据 a 本身,而非副本。

    • 这里的 & 是引用的语法标志,不是取地址,所以 b 直接关联原数据 a,但因为有 const,不能通过 b 修改 a

  2. 当 & 用于表达式中(如 &a,它才表示 “取地址”,获取变量的内存地址

    int x = 10;
    int* p = &x;  // 这里的&是取地址,获取x的内存地址,赋值给指针p

    &x 是表达式中的 &,作用是获取变量 x 在内存中的地址,结果是一个指针值,被赋值给指针变量 p

简单说:声明时的 & 是 “引用”(绑定原数据),表达式中的 & 是 “取地址”(获取内存位置)

回到这里的常量引用:引用一个只读变量的引用,因此不能通过常量引用修改变量的值

const int a = 10;
const int& b = a;  // 声明一个常量引用,引用常量 a
b = 20;  // 非法,无法通过常量引用修改常量 a 的值

5、修饰成员函数

成员函数后加const是 C++ 特殊语法,修饰整个函数不修改成员变量

科普:

对象是一个完整的实体,成员是对象内部的组成部分(数据和方法)

成员函数是:对象的行为、操作方法

对象中成员变量的值的集合叫:对象状态

例如,一个 "人" 对象:

  • 成员变量(状态):年龄、姓名等;
  • 成员函数(行为):吃饭、走路等。

回到这里

class A {
public:
    int func() const {
        // 编译错误,不能修改成员变量的值
        m_value = 10;
        return m_value;
    }
private:
    int m_value;
};

A是类,定义了对象的属性(成员变量)和行为(成员函数)。

而对象是类的实例化结果,比如 A obj; 这句代码会创建一个 A 类的对象 obj

const成员函数的作用是保证该函数不修改对象的状态(即不改变成员变量的值)

这样有个好处是,const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法

核心场景:

const 对象调用成员函数

假设你创建了一个 const 修饰的对象(比如 const A obj; ),因为对象是 “只读” 的,C++ 规定:const 对象只能调用用 const 修饰的成员函数,否则编译器会报错(防止函数里偷偷改对象数据)。

为啥需要 const 成员函数?

给成员函数加 const(像 int func() const ),是在 “承诺”:
这个函数里不会修改对象的成员变量(如果写了 m_value = 10; ,编译器直接报错,强制保证 “只读”)

这样,const 对象调用它时,就不用担心自己的数据被偷偷改掉,符合 “只读” 的逻辑。

比如:

class A {
public:
    // const 成员函数,承诺不修改成员变量
    int func() const {
        // 尝试修改 m_value,编译报错!
        // m_value = 10; 
        return m_value;
    }

    // 普通成员函数(无 const),没承诺“只读”
    void setValue(int v) {
        m_value = v; // 这里会修改成员变量,没问题
    }
private:
    int m_value;
};

int main() {
    const A obj;   // const 对象,“只读”
    obj.func();    // 允许:func 是 const 成员函数,承诺不修改数据
    // obj.setValue(5); // 禁止:setValue 没加 const,可能修改数据,编译器报错
    return 0;
}

volatile用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。编译器会禁止对该变量进行优化而导致出现不符合预期的结果,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。

查看代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

volatile int counter = 0;

void *increment(void *arg) {
    for (int i = 0; i < 100; i++) {
        counter++;
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // 创建两个线程,分别执行increment函数
    pthread_create(&thread1, NULL, increment, NULL);
    pthread_create(&thread2, NULL, increment, NULL);

    // 等待两个线程执行完毕
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Counter: %d\n", counter);
}

未使用 volatile 时,编译器对共享变量读写行为的底层逻辑:

  • 当 CPU 要操作变量(如 counter++ 里的读操作 ),会先看 寄存器 / 缓存 有没有该变量副本。如果有,直接从寄存器 / 缓存读,不会主动去内存重新读

  • 只有当寄存器 / 缓存里没有(或被强制失效),才会从内存读。但编译器优化可能让变量长期 “待” 在寄存器里,程序运行中就跳过内存读,直接用寄存器里的旧值。

  • 变量修改(如自增后),编译器为了性能,可能先把新值放 寄存器 / 缓存 里 “攒着”,不立即写回内存

  • 只有满足特定条件(比如变量要被其他线程访问、寄存器 / 缓存满了 ),才会把寄存器 / 缓存里的新值写回内存。这就导致内存里的变量值,和寄存器 / 缓存里的 “最新值”短暂不一致,其他线程读内存时,拿到的是旧数据。

编译器优化的逻辑是:“能不读内存就不读,能不写内存就不写”,靠寄存器 / 缓存加速。但多线程下,其他线程需要内存里的 “实时值”,这就冲突了。volatile 就是打断这种优化,强制 CPU 每次操作变量都去内存读、写回内存,让内存和寄存器 / 缓存的变量值 “同步”。

有了这个 volatile,每次读写内存

而这volatile 只能解决 “编译器优化导致的可见性问题”(比如让变量每次从内存读、写回内存,不让编译器放寄存器里瞎优化 ),但它管不了 “多线程并行抢着改” 的竞争问题。

counter++ 看着简单,实际是 “读内存→寄存器自增→写回内存” 三步操作。多线程下,线程 A 读了 counter=0 还没写回,线程 B 也读 counter=0,俩人各自自增写回,结果就会少算(本该 +2,实际只 +1 )

所以还要加锁

关于函数为啥void *increment(void *arg) ,

  1. void *increment(void *arg) 是一个线程函数,它的作用是被线程去执行。从函数指针类型角度看,它符合 pthread 库要求的线程函数指针类型 void *(*)(void *) 。这里可以简单理解为,increment 函数名本身就可以当作一个指针(函数指针 ),能传递给需要这种函数指针的地方(比如 pthread_create 函数 ),用于告诉线程要执行哪个函数,不需要额外再写复杂的 (*) 形式(函数名在传递时会自动隐式转换为函数指针 )。

  2. 核心:只要函数的参数、返回值形式和要求的函数指针类型(这里是 void *(*)(void *) 对应形式 )匹配,函数名就能当函数指针用去传递,供像线程创建这类场景使用 

为啥要用函数指针?

(我发现排序算法就是个大炸弹,之前也是 在这栽跟头,看似无脑,但却是开始用指针的一个衔接,之前邝斌那五大算法专题都不需要用指针)

(妈了个逼的的!狗艹的!这里自己拓展学习的各种指针,真的深似海啊)

(狗东西!死妈玩意的指针!总他妈在各种时候卡我,下定决心现在必须操翻搞懂他!!!在此之前啥都不学了!妈逼的!!~~~~(>_<)~~~~)

(真不知道这些大厂人有没有这种感觉,但估计没有,因为就我看到的《鱼皮》、《代码随想录》、《吴师兄》、《公子龙》、《帅地》也都是水货!就他们写那个算法网站,真就一坨屎,他们只能应付面试官,印象很深的头三位里的有个人算法网站里 KMP 肯本没抓重点,甚至让他们去刷 acm 算法题都他妈狗屁不是,一个都过不了,我是可以给他们 A 不过的题,改 bug 到 AC 的,可是这狗逼世道就是投机取巧、乌烟瘴气、骗来骗去,真正打算法竞赛的永无出头之日,这些出来写公众号蹦跶的都是速成狗,没半点真本事,更不用说其他程序员了艹。真的悲哀,我以为程序员搞技术,还算技术认真,发现太天真了。我的感悟就是之前刷算法的时候,网络上所有的讲解都狗鸡巴不是!都他妈错的!现在下定决心艹死这个狗逼指针的时候,跟豆包学,发现之前了解的、学到的、网上口口相传的,很多都他妈是错的,一知半解的!艹!)

(一个这玩意整了4、5天!!)

场景1:

你在玩一个游戏,游戏里有个打怪升级的系统。游戏开发者写了打怪的基本流程代码,但具体怪物被打败后,玩家是获得金币、经验,还是特殊道具,开发者不想把这些逻辑写死。

  • 用函数指针的做法

    游戏开发者定义一个打怪函数,这个函数接收一个函数指针作为参数。不同的游戏策划者可以写自己的 “怪物死亡后奖励函数”, 比如 giveCoin()giveExp() ,然后把这些函数的地址(也就是函数指针)传给打怪函数。这样,每次打怪结束,打怪函数就能根据传入的函数指针,调用对应的奖励函数

  • // 定义奖励函数
    void giveCoin() {
        printf("获得金币\n");
    }
    void giveExp() {
        printf("获得经验\n");
    }
    
    // 打怪函数,接收函数指针作为参数
    void killMonster(void (*callback)()) {
        // 模拟打怪过程
        printf("怪物被打败啦\n");
        // 调用传入的奖励函数
        callback(); 
    }
    
    int main() {
        // 调用打怪函数,并传入获得金币的函数指针
        killMonster(giveCoin); 
    }

场景2:

简单回顾下 冒泡

  • 写一个通用的排序函数,之前刷算法题的时候都是 int 所以写死就行,那么写死 int 有两种写法:

  • 写法一:

    查看代码
    #include <stdio.h>
    void bubbleSort(int arr[], int size) {
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                // 直接在排序函数内部写比较逻辑,不依赖外部cmp函数
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    int main() {
        int numbers[] = {5, 2, 8, 1, 9};
        int count = sizeof(numbers) / sizeof(numbers[0]);
        bubbleSort(numbers, count);
        printf("排序后:");
        for (int i = 0; i < count; i++) 
            printf("%d ", numbers[i]);
    }

    解释:

1、在函数定义中,int* arr 和 int arr[] 完全等价,数组名会自动代表数组首元素的地址,本质就是个指针。

2、sizeof(numbers) 表示计算变量 numbers 所占用的总字节数

3、这代码是不涉及任何指针的,属于传递值,等价于int (*cmp)(int, int) 这种函数指针定义,比较函数里拿到的是副本,比较函数内部直接用值比较,不影响原数据,如果传递指针int (*cmp)(int*, int*),比较函数里拿到的是原数据的地址,可以改变原函数。且在内部需要用 * 解引用才能拿到值。

注意:这里两个int*要么同时有,要么都没有,而第一个cmp*可有可无

  • 写法一进阶:如果说带点指针味道的过度,那就是这个写法

    查看代码

    #include <stdio.h>
    
    // 比较两个int值(直接传值,非指针)
    int compareInt(int a, int b) {
        return a - b;
    }
    
    // 排序函数(直接操作数组元素值)
    void sortInt(int* arr, int size, int (*cmp)(int, int)) {
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                // 直接传递元素值进行比较
                if (cmp(arr[j], arr[j + 1]) > 0) {
                    // 交换元素值
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
    int main() {
        int numbers[] = {5, 2, 8, 1, 9};
        int count = sizeof(numbers) / sizeof(numbers[0]);
        
        sortInt(numbers, count, compareInt);
        
        printf("排序后:");
        for (int i = 0; i < count; i++) {
            printf("%d ", numbers[i]);
        }
    }

解释:

void sortInt(int arr[], int size, int cmp(int, int)) void sortInt(int arr[], int size, int (*cmp)(int, int)) {这两种写法完全等价,都是声明 cmp 指针,而这个指针指向【接收两个 int 参数、返回 int 的函数】,即cmp是 “函数指针”,存的是函数的地址

把函数当参数传递时,“函数类型” 会被隐式转换为 “指向该函数的指针类型”,所以int cmp(int, int)本质就是int (*cmp)(int, int)的简写,编译时会被当作同一种函数指针参数处理,功能上没有任何区别

想让 sortInt 用自己写的 myCmp 函数来比较,得把 myCmp 传给 sortInt。这时候你写 sortInt(arr, 5, myCmp),这里的 myCmp 本身是 “函数类型”,但因为它要当参数传给 sortInt,编译器就自动把它转成 “指向 myCmp 这个函数的指针”了 —— 这也是为啥你写 void sortInt(..., int cmp(int, int)) 能生效,因为编译器知道这里的 cmp 其实是个指针,只是帮你省了写 (*) 的功夫

但注意这两种写法只有出现在函数列表的时候才等价,都表示 “一个指向特定类型函数的指针

但在函数参数列表外,两者完全不同(一个是函数声明,一个是指针变量):

    • 单独写 int cmp(int, int); → 是函数声明(声明了一个接收两个 int、返回 int 的函数

    • 单独写 int (*cmp)(int, int); → 是函数指针变量定义(定义一个能存函数地址的变量)

指针变量的名字是 cmp

单独说 cmp 时,它是指针变量本身(存着函数的地址);用 *cmp 可以间接访问它指向的函数(实际调用时可简化为 cmp(...),编译器会自动处理),例如:int (*cmp)(int, int); 定义后,cmp = add;add 是符合类型的函数),此时 cmp 中存储的就是 add 函数的地址。

    • 比如:

      查看代码
      #include <stdio.h>
      
      // 定义一个符合格式的函数(两个int参数,返回int)
      int compare(int a, int b) {
          return a - b; // 示例:返回差值,用于比较
      }
      
      int main() {
          // 定义函数指针变量cmp,指向"接收两个int、返回int"的函数
          int (*cmp)(int, int);
          
          // 让指针指向具体函数compare(函数名即地址)
          cmp = compare;
          
          // 调用方式1:用指针调用(*cmp等价于cmp)
          int result1 = (*cmp)(3, 5);
          
          // 调用方式2:简化写法(编译器自动处理指针到函数的转换)
          int result2 = cmp(3, 5);
          
          printf("结果1:%d\n", result1); // 输出:-2
          printf("结果2:%d\n", result2); // 输出:-2
      }
    • 发现指针可以当函数来用,至此总结就是:

    1. 函数参数中的函数名自动转换为指针:当函数作为参数传递时(如void sortInt(..., int cmp(int, int))),编译器会自动将函数名隐式转换为函数指针,所以int cmp(int, int)作为参数时,实际等价于int (*cmp)(int, int),这是 C 语言为简化写法的规定。

    2. 调用函数指针时*可省略:代码中(*cmp)(3,5)cmp(3,5)等价,因为编译器会自动处理函数指针的解引用,允许直接用指针名调用函数,这也是 C 语言的语法简化,本质上两者都是通过指针找到函数地址并调用。

    3. 函数指针就是指向函数的指针,直接用这个指针调用函数就直接写这个指针名字加不加*都行,两者的核心都是:C 语言中函数名在大多数场景下会被隐式转换为函数指针,因此可以省略显式的*&(取地址)。例子见此文搜“(后面针对这个代码,会做相当多的分析、深入探讨)”,函数名在多数场景下会隐式转换为函数指针,因此函数指针调用时可省略*,传递函数地址时可省略&,本质都是这种隐式转换的体现。

    4. 函数作为参数传递指的就是函数指针

写死只比较 int 类型的话,有没有指针无区别。指针只是给【既比较 int 也比较 char】用的。那把写死比较 int 类型的搞成指针写法,为下面做铺垫,压压惊开开胃

  • 写法二:

    查看代码
    #include <stdio.h>
    int compareInt(int* a, int* b) {
        return *a - *b;
    }
    void sortInt(int* arr, int size, int (*cmp)(int*, int*)) {
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                if (cmp(&arr[j], &arr[j + 1]) > 0) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    int main() {
        int numbers[] = {5, 2, 8, 1, 9};
        int count = sizeof(numbers) / sizeof(numbers[0]);
        sortInt(numbers, count, compareInt);
        printf("排序后:");
        for (int i = 0; i < count; i++) {
            printf("%d ", numbers[i]);
        }
    }

解释:

    • 这里直接 & 了,是因为明确知道是比较 int,&arr[j]就是第 j 个元素的地址(int*类型),无需额外计算

但到此为止,统说下整个的代码,这里头一次真正自己想搞懂函数指针,也就是指向函数的指针,那这玩意目前刚学不用研究透彻,只知道这个指针是为了当参数就行,那函数当参数刚接触,一屁眼子疑惑,我一一来说,说完就嘎嘎牛逼嘎嘎透彻

    • 首先定义的时候, int *p; 表示 “这是个指针”。但. 当你 “使用变量” 时,* 表示 “解引用”

      int a = 10;
      int *p = &a;

      嘎嘎简单不多逼逼下一个

    • Q:调用的时候bubbleSort(numbers, count); 写成number[]可以吗?显然不行,

A:因为[] 是数组的声明语法,如int numbers[] = {1,2,3};,只能在声明变量或函数参数时候用。而函数调用时,numbers 是已经定义好的数组名,此时它要么代表数组本身,要么被隐式转换为首元素地址,根本不需要也不能加 []

正确的写法只能是 bubbleSort(numbers, count);,这里的 numbers 会被隐式转换为指向首元素的指针(int* 类型),与函定义的数参数 int arr[](本质是 int*)完美匹配。

Q:那int arr[]到底是啥?

A:int arr[] 是一种数组形式的指针声明,本质上等价于 int *arr,比如void bubbleSort(int arr[], int size) { ... },这里的 int arr[] 看起来是数组参数,实际上和 int *arr 完全一样,函数内部操作的是指向数组首元素的指针,通过它来访问数组元素(如 arr[j] 等价于 *(arr + j))。

Q:这些我早都知道,但现在涉及了更多的东西反而糊涂了!!arrarr[]*,都是啥啊

A:再深入说原理:

声明场景:你在 “告诉编译器” 这个东西是什么(是数组?是指针?),需要写清楚 “类型规则”;

调用场景:你在 “实际用” 已经声明好的东西,只需要写 “东西的名字”,不用再写类型规则。

场景 1:声明(函数参数 / 变量定义)—— 这里才需要 arr[] 或 *

image

场景 2:调用(传函数参数)—— 这里只需要写 “名字”,绝对不能加 [] 或 *

image

总结就是:

调用函数时,绝对不能写 arr[]:比如 bubbleSort(numbers[], count) 是错的 ——[] 是声明时用的 “类型规则”,调用时只需要传数组名(numbers);

调用函数时,除非特殊需求(比如传指针的地址),否则不写 *:比如 bubbleSort(*numbers, count) 是错的 ——* 是 “解引用”(拿指针指向的值),这里需要传地址,不是值;

声明函数参数时,int arr[] 和 int *arr 随便写,完全等价:不用纠结哪个对,重点是知道它们本质都是指针,用来接收数组首元素的地址。

用冒泡排序代码验证一下:

声明函数时:int arr[](对)、int *arr(对);

调用函数时:numbers(对)、numbers[](错)、*numbers(错)。

Q:那为啥啊?

A:其实是 C 语言设计时的 “妥协” 与 “简化” —— 既要让代码写起来直观(能体现 “操作数组” 的意图),又要避免内存浪费,最终形成了 “声明和调用时用法不一样” 的规则。

1. 为什么 “声明函数参数时”,int arr[] 能存在(还等价于 int *arr)?

本质是 C 语言不想让你 “传整个数组” —— 如果真的允许 “传整个数组”,会发生什么?比如你定义了 int numbers[1000](占 4000 字节),如果调用函数时要把这 4000 字节完整拷贝一份传给函数,会严重浪费内存和运行时间。

所以 C 语言做了个 “规定”:数组作为函数参数时,自动 “退化” 成指向首元素的指针(只传 4/8 字节的地址,不拷贝整个数组)。

但直接写 int *arr 又有个问题:程序员看代码时,可能不知道这个指针是用来处理数组的(比如你写 bubbleSort(int *arr, ...),别人可能误以为是 “处理单个指针”,不是 “处理数组”)。

于是 C 语言又留了个 “人性化语法”:允许你写 int arr[] 来声明参数 —— 它看起来像数组,能明确告诉读者 “这里要传一个数组”,但编译器会悄悄把它解读成 int *arr(本质还是指针)。

总结:int arr[] 是 “给人看的直观写法”,int *arr 是 “给编译器看的真实写法”,二者在函数参数声明里完全等价。

2. 为什么 “调用函数时”,不能写 arr[] 或 *arr

因为 调用时的核心是 “传正确的值”,而 [] 和 * 在这里都是 “多余的干扰”

先看 [][] 是 “声明时的语法符号”(用来告诉编译器 “这是数组类型”),比如 int numbers[] = {1,2} 里的 [],是在定义数组时用的。而调用函数时,numbers 已经是一个 “现成的数组名” 了,它要么代表数组本身,要么会自动转成首元素指针 —— 你再写 numbers[],编译器会懵:“[] 在这里干嘛?是要定义新数组吗?可这里是函数调用啊!” 所以直接报错。

再看 ** 是 “解引用符号”,作用是 “通过指针拿它指向的值”。比如你有指针 int *p = numbers(p 指向首元素 5),*p 就等于 5(首元素的值)。而函数需要的是 “数组首元素的地址”(比如 numbers 自动转成的地址),如果你传 *numbers,相当于传了 “首元素的值 5”—— 函数拿到 5 后,会把 5 当成地址去访问内存(比如 arr[j] 变成 *(5 + j)),这会直接越界崩溃,完全不符合需求。

总结:调用时只需要传 “数组名”(比如 numbers)—— 它会自动转成函数需要的首元素指针,加 [] 是语法错误,加 * 是传错了值(传了元素值,不是地址)。

C 语言为了 兼顾 “代码直观性” 和 “内存效率”,在 “声明时” 允许用 int arr[] 伪装数组(实际是指针),让你知道要处理数组;在 “调用时” 只让你传数组名(自动转地址),避免多余语法导致错误 —— 本质是 “语法设计服务于实际需求” 的结果。

懂了很多!

    • 再说点东西,int arr[3] = {1, 2, 3};

1、arr数组名在大多数情况下会被隐式转换为 “指向数组首元素的指针”(int* 类型),等价于 &arr[0]。例如:arr 和 &arr[0] 地址相同,都指向第一个元素 1

2、&arr表示 “指向整个数组的指针”(int(*)[3] 类型,即指向包含仨 int 的数组的指针)。它的地址和 arr 相同,但含义不同:&arr + 1 会跳过整个数组(移动 3*4=12 字节),而 arr + 1 只跳过一个元素(移动 4 字节)。

3、*arr等价于 arr[0],表示数组的第一个元素(值为 1)。因为 arr 是首元素指针,*arr 就是解引用这个指针,得到首元素的值。

4、arr[]这是数组的声明形式(如 int arr[] = {1,2,3}),表示定义一个数组,编译器会根据初始化内容推断长度。

简单总结:

arr ≡ &arr[0](首元素指针)

&arr 是整个数组的指针(地址相同,类型不同)

*arr ≡ arr[0](首元素的值)

arr[] 是数组的声明语法

数组和函数的语法规则不同,数组名的转换更复杂,核心是区分 “指向元素的指针” 和 “指向整个数组的指针”

Q:“指向整个数组的指针”咋理解?

A:假设你有一个 “装 3 个苹果的盒子”(对应代码里的 int arr[3],3 个 int 元素的数组)。

“指向苹果的指针”:是指着盒子里某一个苹果(比如第一个苹果),关注的是 “单个元素”;

“指向整个数组的指针”:是指着整个盒子,关注的是 “装 3 个苹果的整体”—— 它的 “目标” 不是单个元素,而是整个数组。

再回到代码,用 int arr[3] = {1,2,3} 举例:

1. 先明确 int(*)[3] 这个类型的含义

int(*)[3] 是一个 “指针类型”,翻译过来就是:

最里面的 [3] 表示 “指向的目标是一个包含 3 个元素的数组”;

前面的 int 表示 “这个数组里的每个元素是 int 类型”;

括号 (*) 是为了强调 “这是一个指针”(如果写成 int*[3] 就变成 “指针数组” 了,完全不同)。

所以 int(*)[3] 的本质:专门用来指向 “包含 3 个 int 的数组” 的指针,它的 “目标单位” 是 “整个数组”,不是单个 int。

2. 用代码看区别:地址相同,但 “步长” 不同(关键!)

虽然 arr(首元素指针)和 &arr(整个数组指针)的起始地址数值完全一样(都指向数组开头),但它们的 “步长”(指针 + 1 时移动的字节数)完全不同 —— 这就是 “指向单个元素” 和 “指向整个数组” 的核心差异:

      • #include <stdio.h>
        int main() {
            int arr[3] = {1,2,3};
            
            // 1. 打印地址(数值相同)
            printf("arr的地址:%p\n", arr);       // 比如输出 0x7ffeeabc1230
            printf("&arr的地址:%p\n", &arr);     // 同样输出 0x7ffeeabc1230(地址数值一样)
            
            // 2. 看指针+1后的差异(步长不同)
            printf("arr+1的地址:%p\n", arr+1);   // 0x7ffeeabc1234(移动4字节,跳过1个int)
            printf("&arr+1的地址:%p\n", &arr+1); // 0x7ffeeabc123c(移动12字节,跳过3个int)
            return 0;
        }
      • arr 是 int* 类型(指向单个 int),+1 时移动 “1 个 int 的大小”(4 字节),只跳过 1 个元素;

      • &arr 是 int(*)[3] 类型(指向整个数组),+1 时移动 “整个数组的大小”(3*4=12 字节),直接跳过整个数组。

int(*)[3] 这种指针,就像给 “装 3 个 int 的数组” 贴了个 “专属标签”—— 它只认 “整个数组” 当目标,所以操作时(比如 + 1)也是按 “整个数组” 的尺寸来算,和普通的 “指向单个 int 的指针”(int*)完全不是一回事

而指针变量的名字就夹在 * 和 [ ] 中间。比如还是用 int arr[3] = {1,2,3};,要定义一个 “指向整个数组的指针变量”,写法是:int (*p_arr)[3] = &arr;, 看看咋用

      • 代码:

        查看代码
        #include <stdio.h>
        int main() {
            int arr[3] = {1,2,3};
            // 定义“指向整个数组的指针”,名字叫 p_arr
            int (*p_arr)[3] = &arr; 
        
            // 访问数组元素的两种方式(本质都是通过指针找元素)
            // 1. 先解引用 p_arr(得到整个数组 arr),再用 [ ] 取元素
            printf("第一个元素:%d\n", (*p_arr)[0]); // 等价于 arr[0],输出 1
            printf("第二个元素:%d\n", (*p_arr)[1]); // 等价于 arr[1],输出 2
        
            // 2. 也可以通过指针偏移(但因为 p_arr 是数组指针,偏移要注意步长)
            // 这里 p_arr 指向 arr,p_arr+1 会跳过整个 arr,所以很少这么用
        }

这个指针和普通指针(比如 int* p,p 是名字)一样,“指向整个数组的指针” 也有自己的名字,只是声明时要把名字放在 (*) 里,保证它先和 * 结合,成为指针变量

把 arr 想象成一个装了 3 个苹果的快递箱,箱子里有 3 个格子,分别放着苹果 1、苹果 2、苹果 3(对应 arr[0]arr[1]arr[2]);这个箱子本身就叫 arr(整个数组),它不是单个苹果,而是 “装 3 个苹果的整体”。

(*p_arr) 为什么是 “整个数组 arr”?

* 在这里是 “解引用”,意思是 “根据指针找到它指的东西”:

p_arr 指着 “arr 快递箱”,所以 *p_arr 就是 “找到这个快递箱本身”(整个箱子,不是单个苹果);

就像你根据标签上的地址找到快递箱,*p_arr 就是 “那个箱子本身”—— 也就是 arr 数组整体

 (*p_arr)[0] 为什么是 arr[0]?先通过 *p_arr 找到 “整个快递箱”(就是 arr);再用 [0] 从箱子里拿出第一个苹果 —— 和直接从 arr 里拿 arr[0] 完全一样。

    • 好,至此透彻了一些,再继续看点其他的。关于数组名到底是个啥鸡巴玩意?众所周知的就是:数组名是指针,是指向首地址的一个指针,但大错特错!其实扯了这么多最开始是因为我觉得 arr 这个数组名是指针,但其实不是,所以才引出了这么多追问!(数组名不是指针引发的血案)

咱们得把 “数组名的本质” 和 “编译器对数组名的使用规则” 拆开来,就像分清 “一张纸本身” 和 “我们在纸上写什么”

比如写 int arr[3] = {1,2,3};,编译器会做两件事:

申请一块 12 字节的内存(int 占 4 字节 × 3 个元素),用来存 1、2、3 这三个值;

给这块内存贴一个 “标签”,就是 arr—— 它的作用只有一个:告诉编译器 “这片 12 字节的内存叫 arr”

数组名 arr 不占用额外内存:它不是像指针变量(比如 int *p)那样,需要额外 4/8 字节存地址;arr 只是个 “标识”,就像你家大门上的 “301” 门牌号,门牌号本身不占额外空间,只是用来指代你家那套房子。

数组名 arr 本身不 “存” 地址:它自己不是 “装地址的容器”,但它能 “对应” 一个地址 —— 就是这片 12 字节内存的起始位置(和首元素 arr[0] 的地址数值相同),因为标签必然贴在内存块的开头。

再解释之前的矛盾:“数组名不存地址”,但为啥能 “用成地址”?

这是编译器的 “特殊处理”—— 为了让我们能方便操作数组元素,编译器规定:当你在 “需要地址的场景” 里用数组名 arr 时,它会自动把 arr 当成 “这片内存的起始地址”(也就是首元素 arr[0] 的地址)来用。

int *p = arr;:这里需要给指针 p 赋值一个地址,编译器就把 arr 对应的 “内存起始地址” 赋给 p(相当于 int *p = &arr[0];);

printf("%d", arr[1]);:本质是先通过 arr 找到内存起始地址,再往后偏移 4 字节(一个 int),取对应的值 —— 这里 arr 也被当成地址用了。

这种 “当成地址用”,是编译器的 “临时转换”,不是说 arr 本身就是地址。 

最后回到 “&arr”:为啥它是 “整个数组的地址”?

& 符号的作用是 “获取变量的地址”,但这里的 “变量” 要广义理解 ——arr 代表的 “整个 12 字节内存块”,也算是一个 “整体变量”(类型是 int[3],不是单个 int)。

所以 &arr 的意思是:获取 “arr 这个 12 字节内存块” 作为一个整体的地址。它和 “arr 被转成的首元素地址”(&arr[0])的区别,就像:

你家 “301 这套房子的地址”(&arr):指向的是 “整个房子”(120 平米);

你家 “大门的地址”(arr 转成的地址):指向的是 “房子的入口”(对应首元素)。

数值上两者可能一样(都是你家所在的楼栋门牌号),但 “指向的范围” 完全不同 ——&arr+1 会跳过整个 12 字节(整个房子),而 arr+1 只跳过 4 字节(一个 int,相当于房子里的一个房间)。

一句话总结,彻底理清:

arr 本身:是 “12 字节内存块的标签”,不占内存、不存地址,只代表 “这片内存”;

用 arr 时:编译器自动把它转成 “这片内存的起始地址”(首元素地址),方便操作元素;

&arr:是 “获取整个内存块的地址”,指向的是 “整个数组”,不是单个元素。

所以,编译器为了省事,就把 arr 自动转成 “第一个元素的地址”(&arr[0]),不用你每次都写 &arr[0]—— 这就是 “隐式转换”。

但你要记住:转换后是 “首元素地址”,不代表 arr 本身就是这个地址。就像你说 “去 XX 街 100 号”,默认是找第一个快递盒,但 “XX 街 100 号” 本身还是 “整个快递站”,不是 “第一个快递盒”。

“arr 转的首元素地址” vs “&arr 的数组地址”

还是快递站例子:arr 转成的 “首元素地址”:相当于 “XX 街 100 号快递站的第一个快递盒”(指向单个元素);&arr 的 “数组地址”:相当于 “XX 街 100 号快递站”(指向整个快递站)。数值上两者可能一样(比如都是 0x1234),但 “指向的范围” 完全不同:

如果你从 “第一个快递盒” 往后跳 1 步(arr+1),就到 “第二个快递盒”;

如果你从 “整个快递站” 往后跳 1 步(&arr+1),就跳过整个快递站,到 “下一个快递站”(跳过 3 个元素)。

arr 能隐式转成首元素地址,是编译器的 “方便操作”,不代表 arr 本身是地址(它还是整个数组);

arr 有地址,就是 &arr—— 这个地址指向 “整个数组”,和 “隐式转出来的首元素地址” 不是一回事

零零碎碎说了一堆终于搞懂了

再墨迹两句:

数组名没地址 和 int arr[] 的关系:

int arr [] 是声明数组,意思是我要一块内存,用来装多个 int,给它起个名叫 arr。这里的 arr 就像给那片内存贴的标签,标签本身不占地方(内存),只是方便你指代那片内存。

数组名没地址是说:这个标签 arr 本身不在内存里存着,所以你没法取标签自己的地址,就像你不能问“3 号楼”这三个字写在哪个位置,因为它只是个名字。但标签 arr 对应的那片内存(整个数组)是有地址的

所以,数组名是指针是错的!!

数组名是 “数组这片内存的名字”,是“一块内存的标识”,它不占用额外内存,也不能被赋值(你不能写arr = 其他地址)。

指针是 “存地址的变量”,占用 4/8 字节内存,里面存的是另一个内存的地址,而且可以被赋值(p = &arr[0]是合法的)。

—— 两者本质完全不一样。

数组名arr = “3 号楼” 这个名字(用来指代整栋楼,本身不是实物,不占地方);指针变量int* p = 一张小纸条(能写字,纸上写的 “3 号楼 101” 就是地址,纸条本身占地方)。

哪怕arr有时候会被 “自动当成首元素地址”(比如int* p = arr),也只是 “用起来像指针”,不是 “它本身是指针”—— 就像你用手机扫码付款,手机 “用起来像钱包”,但手机本身不是钱包。

妈逼的现在豆包更新了回答方式,会显示出两种,且无法删除、编辑啥的

再次总结,重复加深印象:

关于 arr 数组名:

arr 就等于用 “第一个元素的地址”(&arr[0]),但有 2 个特殊情况(arr 代表整个数组):

用在 sizeof(arr) 里:算的是整个数组的总大小(比如 3 个 int 就是 12 字节)。

用在 &arr 里:&arr 是 “整个数组的地址”

关于 &arr

类型是 int(*)[3](指向整个数组的指针),专门用来 “指整个数组”。

arr 的区别:arr + 1 跳 1 个元素(4 字节);&arr + 1 跳整个数组(12 字节)。

关于 *arr

就是 “第一个元素的值”(arr[0])。因为 arr 是第一个元素的地址,*arr 就是 “解引用” 这个地址,得到值。

关于 arr[]

只在声明数组时用(比如 int arr[] = {1,2,3}),表示 “这是个数组”,编译器会自己算长度

再继续说几个 :

&arr 的类型正好是 int(*)[3],和 “指向整个数组的指针” 类型完全匹配

      • int arr[3] = {1,2,3};
        int (*p)[3] = &arr; // 正确:类型完全匹配
        int (*p)[3] = arr;  // 错误:arr是int*类型,和int(*)[3]不兼容

数组名(比如 arr)更像一个 “标签”,它本身不占用内存,也没有自己的地址—— 它的作用就是 “标记” 一片固定的内存区域,这片区域就是数组实际存储元素的地方

但数组名有类型,

比如:int arr[3] 中,arr 的类型是 “包含 3 个 int 元素的数组”,写作 int[3], 即数组名是一个数组类型,读作“包含 3 个 int 元素的数组类型”,但当你写 arr + 1*arrarr[0] 时,arr 会被隐式转换成 int* 类型(指向首元素 arr[0] 的指针);但这是 “临时转换”,不改变 arr 本身是 int[3] 数组类型的本质

比如:char str[10] 中,str 的类型是 “包含 10 个 char 元素的数组”(写作 char[10])。

唯一例外:当数组名用在 sizeof(arr) 或 &arr 中时,不会转换为指针:

sizeof(arr) 计算的是整个数组的大小(比如 int[3] 就是 12 字节);

&arr 得到的是「指向整个数组的指针」(类型是 int(*)[3]),这时候 &arr + 1 会跳过整个数组的大小

说明:数组名没地址,没内存,但却有类型。

“类型” 是编译器给的 “身份标签”:用来告诉编译器 “这个东西是什么、该怎么用”(比如是数组还是指针、能存多少元素、运算时该怎么处理);

“内存 / 地址” 是这个东西在电脑里的 “物理位置”:只有需要 “存数据” 的东西(比如变量)才会占内存、有地址。

你家小区门口的 “1 号楼” 是个 “标识”(类似数组名arr):它有 “类型”:是 “一栋 18 层的居民楼”(类似arr的类型是 “3 个 int 的数组”)—— 这个 “类型” 决定了它是 “楼” 不是 “车”,能住多少人;但 “1 号楼” 这个标识本身,不占物理空间(不会在小区里找个地方 “放这个名字”),也没有 “自己的地址”(你不会说 “1 号楼这个名字在小区的哪个位置”)—— 它只是用来指代那栋实际存在的楼(类似arr指代那片存数组元素的内存)。

回到数组名arr

为什么有类型?

编译器需要知道arr是 “数组”(不是指针 / 变量)、元素是int、长度是 3—— 这样才能正确计算sizeof(arr)(3×4=12 字节)、判断&arr的指针类型(必须是int(*)[3])、处理arr的隐式转换(转成int*指向首元素)。如果没有类型,编译器根本不知道该怎么用arr

为什么没内存 / 地址?

arr只是个 “标识”,用来指代那片存arr[0]、arr[1]、arr[2]的内存(这片内存有地址)。arr本身不存任何数据(不像指针变量int* p,需要占 4/8 字节存地址),所以它不需要内存,也没有自己的地址。就像 “1 号楼” 这个名字不占空间,但不妨碍它有 “居民楼” 的类型;arr没内存 / 地址,也不妨碍它有 “int [3] 数组” 的类型 —— 两者完全不冲突

    • 说完了 数组名 到底是个鸡巴啥玩意,再说下 传参、定义、调用 相关的一些东西,:

传参时(实参):函数名本身就代表函数的地址,和加&取地址的效果完全一样。这是为了写代码更方便,不用每次传函数都手动加&。函数名 ≡ & 函数名

声明时(形参):写int cmp(int, int)时,编译器会自动把它当成函数指针int (*cmp)(int, int)来处理。为了简化写法,让函数指针的声明看起来更像普通函数声明,降低理解难度。int 函数名 (参数) ≡ int (* 函数名)(参数) 

      • 代码:(后面针对这个代码,会做相当多的分析、深入探讨)

        查看代码
        #include <stdio.h>
        #include<iostream>
        using namespace std
            
        // 1. 普通函数(要被传递的函数)
        int compare(int a, int b) { return a - b; }
        
        // 2. 形参声明:两种写法等价(都是声明“cmp是函数指针”)
        // 写法A:没写*,编译器自动补成指针
        void sort(int cmp(int, int)) { 
            // 这里用 cmp 其实就是用函数指针,和下面写法B的 (*cmp) 效果一样
            int res = cmp(1, 2); 
        }
        // 写法B:显式写*,明确表示cmp是函数指针(和写法A完全等价)
        void sort(int (*cmp)(int, int)) { //*是指针声明的一部分,用来表示cmp是函数指针,不是解引用。这里*和括号结合,是声明语法的一部分。
            int res = (*cmp)(1, 2); //:*作用于已声明的指针变量cmp,表示通过指针访问目标函数,是解引用,和上面的 cmp(1,2) 效果一样,因为规定函数指针在调用时,编译器会自动处理解引用操作。
        }
        
        // 3. 实参传递:两种写法等价(都是传“compare函数的地址”)
        int main() {
            sort(compare);   // 写法1:函数名自动视为地址(等价于&compare)
            sort(&compare);  // 写法2:显式取地址(和上面完全等价)
            sort(*compare);  //写法3:单独说
        }

解释:

函数名本质就是函数的入口地址

Q:我的思考是,这里怎么一会&一会*,不都是调用吗?

A:但其实不是!int res = cmp(1, 2); 是通过函数指针调用函数(执行函数逻辑,得到返回值)。sort(compare);是传递函数地址作为参数(将函数作为参数传给另一个函数,不执行函数逻辑)

Q:不都是调用的步骤吗?

A:不是,本质区别是“是否执行函数逻辑”

int res = cmp(1, 2);:是调用函数—— 会执行 cmp 指向的函数(比如 compare),计算 1-2 并返回结果,最终把值赋给 res,这一步有函数的实际执行过程。

sort(compare);是传递函数地址—— 仅把 compare 的地址传给 sort 的参数 cmp,不执行 compare 函数,只是让 sort 内部能通过这个地址找到 compare,后续由 sort 内部决定何时调用(比如 sort 里的 cmp(1,2)才是调用 compare 函数)。这步只是调用sort 函数,而compare是作为参数传递给 sort,而非调用 compare 函数。

Q:那如果sort有返回值,把这个返回值给一个变量,那sort(compare);就要写成比如int a = sort(*compare)

A:狗娘养的死全家的豆包用阳寿回答问题!艹!耽误我一天!一开始说的是错误结论:

无论sort是否有返回值,传递函数地址时都用sort(compare)sort(&compare),绝不能写成sort(*compare)compare是函数名,隐式转为指针(地址)。*compare是对指针解引用,得到的是函数本身,而函数不能直接作为参数传递,假设sort返回int,也是int a = sort(compare);int a = sort(&compare);

后来自己实践又追问,发现可以写sort(*compare)。任何语言都 绝对不能传递 “整个函数实体”,只能传递函数的地址(入口地址)”。sort(*compare) 本质还是传递地址。当你写 *compare 时,看似是 “对函数指针解引用得到函数本身”,但 C 语言编译器有个特殊规则:对 “函数地址 / 函数指针” 做解引用(*)后,编译器会自动把结果再转成函数地址。所以 *compare 最终的效果,和 compare(函数名,本身就是地址)、&compare(显式取地址)完全一样 —— 都是传递 “函数的地址”,而非 “整个函数”。

三种传参写法的本质等价(但推荐度不同):

sort(compare):函数名隐式转为地址,简洁规范,推荐使用

sort(&compare):显式取地址,语法正确但冗余(函数名已代表地址);

sort(*compare):看似解引用 “得到函数本身”,但编译器会自动转成地址,语法合法但逻辑冗余、可读性差,绝对不推荐

Q:和 int res = (*cmp)(1, 2);的差别?

A:int res = (*cmp)(1, 2); 是通过函数指针调用函数(执行函数逻辑),而 sort(compare) 是传递函数地址(不执行函数),核心区别在于是否执行函数体:

(*cmp)(1, 2)cmp 是函数指针变量(指向某个函数,比如 compare)。*cmp 解引用指针,得到函数本身,再加 (1,2) 就是调用该函数。会实际执行 compare(1,2) 的逻辑,返回结果给 res

sort(compare):仅将 compare 的地址传给 sort 函数的参数(cmp 指针)。不执行 compare 函数,compare 此时只是一个 “地址值”。sort 内部会用这个地址,在需要时通过 cmp(1,2) 或 (*cmp)(1,2) 调用函数

简单说:前者是 “调用函数并获取结果”,后者是 “传递函数地址供后续使用”。

Q:那为啥加了返回值,比如 int a = sort(compare);和 int res = (*cmp)(1, 2); 也不一样?

A:int res = (*cmp)(1, 2); 是通过函数指针调用函数并获取返回值,这和传递函数地址是完全不同的操作:

(*cmp)(1, 2) 中,* 是对函数指针 cmp 的解引用,(1,2) 是传递参数,整体会执行 cmp 指向的函数(比如 compare),并将结果存到 res 中。

而传递函数地址(如 sort(compare))时,既没有解引用,也没有加参数列表 (),所以不会执行函数,只是把函数的地址传过去。

就算 int a = sort(compare); int a = sort(&compare); 甚至不规范但 VS 不会报错的写法:int a = sort(*compare); 这三个哪怕最后一个也和 int res = (*cmp)(1, 2);void sort(int (*cmp)(int, int))完全不同? 因为void sort(int (*cmp)(int, int))是定义指向函数的指针,也叫函数指针,说白点就是定义指针! 而int res = (*cmp)(1, 2);是通过指针调用现成的函数,而int a = sort(*compare); 也是函数调用,但是调用 sort 函数,传入的 compare 是实参(函数地址),而int res = (*cmp)(1, 2); 是在 sort 函数内部,通过函数指针 cmp 调用它指向的函数(比如 compare)。

总结就是int a = sort(compare);是调用sort函数,属于函数套函数。而int res = (*cmp)(1, 2);通过指针在sort函数内部调用 compare直接一层调用

Q:那加了参数呢?

A:如果加了参数,就变成了函数调用表达式,传递的不再是函数地址,而是函数的返回值。

      • int compare(int a, int b) { return a - b; }
        void sort(int (*cmp)(int, int));
        
        // 错误示例:传递的是 compare(3,5) 的返回值(-2),而非函数地址
        sort(compare(3, 5));  // 错误!参数类型不匹配(需要函数指针,实际传了int)

Q:那这时候可以用*compare吗?即 sort(*compare(3, 5));

A:即使加了参数,用*compare也不对。*compare本质是对函数名的解引用(函数名隐式转为指针后再解引用),结果仍是函数本身。

如果写成(*compare)(3,5),这和compare(3,5)完全等价,都是调用函数并返回结果(比如 - 2),依然是传递返回值而非函数地址,照样会导致sort的参数类型不匹配。

正确传递函数地址的方式始终是:sort(compare) 或 sort(&compare)(不加参数)

    • 到此说完,另外再说下 C 没有的,C++ 有的“起别名”也叫函数引用,用法是 int (&cmp)(int, int)

首先,解引用是对指针(地址)说的

C++ 的引用,就是起别名,这是通过 “关联关系” 在编译期确定的 “硬绑定”,不需要像指针那样用地址来 “间接找到” 原对象。

所以C++ 的函数引用不是地址,和 “原始数据的地址” 毫无关系。不存在 “通过地址间接访问” 的过程。

声明后直接用 cmp(1,2) 调用,不能写成 (*cmp)(1,2)(引用没有地址,不需要解引用)。

传递时直接传函数名,比如 sort(cmp),但这里 cmp 是引用,不是地址,类型由“被引用对象的类型” 决定,对 int 变量的引用,类型是 int&,对函数 int add(int, int) 的引用,类型是 int(&)(int, int)

C 没有引用的东西,只有对地址、指针解引用,没“引用”概念。

      • 代码:
        查看代码
        #include <iostream>
        
        // 1. 先定义一个普通函数(比如计算两数之和)
        int sum(int x, int y) {
            return x + y;
        }
        
        int main() {
            // 2. 声明函数引用:ref_sum 是 sum 函数的“别名”
            int (&ref_sum)(int, int) = sum;
        
            // 3. 通过函数引用调用(和直接调用 sum 完全一样)
            int result = ref_sum(2, 3);  // 等价于 sum(2, 3)
        
            // 输出结果
            std::cout << "结果:" << result << std::endl;  // 最终打印“结果:5”
        }

int (&ref_sum)(int, int) = sum;ref_sum 不是指针,就是 sum 函数的 “另一个名字”,调用 ref_sum 和调用 sum 没有任何区别

    • 继续深入捋顺梳理:

Q:之前说的,为何sort(compare); sort(&compare);是这样,int res = (*cmp)(1, 2);就是加*的,我理解都是调用啊?

A:

本质 1:先明确两个核心场景 ——“声明函数指针” vs “调用函数指针”

你混淆的根本,是没分清*在 “定义指针变量” 和 “用指针调用函数” 时的不同意义:

场景 1:声明(定义)函数指针变量(比如int (*cmp)(int, int);

这里的*声明语法的一部分,作用是 “告诉编译器:cmp不是普通变量,而是一个 “指向函数的指针””。

语法结构拆解:(int (*cmp)(int, int))

括号(*cmp)必须加,否则int *cmp(int, int)会变成 “返回 int * 的函数”(完全错了)。

这里的*不是 “解引用操作”,只是用来标记cmp的身份 —— 它是指针,指向 “参数为两个 int、返回值为 int 的函数”。

这一步是 “定义工具”,就像你买了个能夹核桃的钳子(定义cmp这个指针),还没开始夹核桃。

int cmp(int, int) 这种写法,会被编译器隐式转换为函数指针类型,最终等价于 int (*cmp)(int, int)(函数指针)

场景 2:用函数指针调用函数(比如(*cmp)(1,2) 或 cmp(1,2)

这里的*解引用操作符,作用是 “通过指针cmp存储的地址,找到它指向的函数,然后调用这个函数”。

本质逻辑:cmp里存着某个函数(比如compare)的地址 → *cmp就是 “通过地址找到的那个函数本身” → 加(1,2)就是调用这个函数。

C 语言允许偷懒:因为函数名本身就等于它的地址,所以cmp(1,2)会被编译器自动当成 “通过cmp的地址调用函数”,和(*cmp)(1,2)功能完全一样(语法糖)。

这一步是 “使用工具”,就是用钳子夹核桃(调用函数),会出结果(核桃碎了 / 函数返回值)。

本质 2:你之前混乱的 “差别”,其实是 “工具定义” 和 “工具使用” 的差别

你之前纠结的 “和*cmp的差别”,本质是:

如果你指的是 “声明时的int (*cmp)(...)”:这是 “定义指针工具”,没执行任何函数,只是告诉编译器 “cmp是干嘛的”;

如果你指的是 “调用时的(*cmp)(1,2)”:这是 “用指针工具调用函数”,会执行函数逻辑,返回结果;

而你之前提的sort(compare):这是 “把函数的地址传给别人用”,是传递函数地址,既不是定义指针,也不是调用函数,只是 “把工具(函数)的地址递出去”。

一句话总结:

声明函数指针时(int (*cmp)(int,int);):*是 “身份标记”,用来定义cmp是函数指针;

调用函数指针时((*cmp)(1,2)):*是 “解引用操作”,用来通过指针找到函数并执行;

这俩是 “定义工具” 和 “用工具干活” 的根本区别,跟 “传地址”(sort(compare))更是两码事

    • 再次逼逼巩固:

传递函数地址时,&函数名函数名等价(省略&),比如:sort(compare) 等价于 sort(&compare)

用函数指针调用函数时,指针名() 和 (*指针名)() 等价(省略*),比如:cmp(1,2) 等价于 (*cmp)(1,2)

    • 懂了这些最基础的再说代码:

注意以上都是围绕此文搜“(后面针对这个代码,会做相当多的分析、深入探讨)”来说的

int (*cmp)(int, int)指向的是“参数为两个 int、返回值为 int 的函数” 的指针,变量名叫 cmp,但不是指向cmp的指针,这个指针可以指向任何符合 “两个 int 参数、int 返回值” 的函数,不固定指向某个特定叫cmp的函数。具体指向谁是调用时传的一个符合格式的函数名。

  • 开始上最通用的,写法三:(妈逼的这里有超级大的学问)

    查看代码
    // 整数比较函数
    int compareInt(const void* a, const void* b) {
        return *(int*)a - *(int*)b;
    }
    
    // 字符串比较函数
    int compareString(const void* a, const void* b) {
        return strcmp(*(char**)a, *(char**)b);
    }
    
    // 通用排序函数,接收比较函数指针
    void mySort(void* arr, int size, int elementSize, int (*compare)(const void*, const void*)) {
        // 这里省略具体排序实现,假设用冒泡排序思路
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                char* p1 = (char*)arr + j * elementSize;
                char* p2 = (char*)arr + (j + 1) * elementSize;
                if (compare(p1, p2) > 0) {
                    // 交换元素
                    char temp[elementSize];
                    memcpy(temp, p1, elementSize);
                    memcpy(p1, p2, elementSize);
                    memcpy(p2, temp, elementSize);
                }
            }
        }
    }
    
    int main() {
        int intArr[] = { 5, 3, 1 };
        mySort(intArr, 3, sizeof(int), compareInt); 
        char* strArr[] = { "cat", "apple", "banana" };
        mySort(strArr, 3, sizeof(char*), compareString); 
        return 0;
    }

接收一个比较函数的指针作为参数。

排整数数组时,传入整数比较函数,

排字符串数组时,传入字符串比较函数。

这样,同一个排序函数,通过不同的比较函数指针,就能处理不同类型数据的排序。

解释:(一直追问到了女娲补天、盘古开天辟地的最底层设计逻辑理论知识,然后又从这些来到现在的知识点,反复抽插,打通所有疑惑)

通用的mySort要支持任意类型必须用char*做基地址(最小字节单位),通过j * elementSize计算偏移量,这样才能适配不同大小的元素(int 占 4 字节,char * 占 8 字节等)

这里的排序函数mySort里就不能再用&了,因为它要处理任意类型的数组,而&的用法依赖于具体类型。

比如对于int数组,因为知道每个元素是int类型,&arr[j]能正确拿到第 j 个元素的地址

但通用函数mySort的参数是void* arr(丢失了具体类型信息),编译器无法知道arr指向的数组元素是什么类型,也就无法通过&arr[j]来计算地址,j的偏移量取决于元素大小,&arr[j]能否用,取决于arr的类型是否明确,而void本身没有类型没有大小

这里就是手动算大小,所以通用函数必须用char*做基地址,配合elementSize(元素字节数),通过j * elementSize这种字节级的偏移计算来定位元素,不管元素是什么类型,都能正确找到地址。

而专用函数sortInt因为知道是int数组,所以可以直接用&arr[j]—— 本质是因为它提前知道了元素类型和大小。

p1的类型?

是指针类型,具体来说是char*类型(字符指针类型)。这个是永远不变写死的

指针类型本身包含两重信息:

1、它是指针(用来存储地址);

2、它的基类型是char(决定了指针运算的偏移量,比如p1+1会偏移 1 字节,而如果p1int*,那p1+1会偏移 4 字节)

p1指向的东西?

因为arrint数组,j * elementSize(比如 j=1 时就是 4 字节)计算出的偏移,让 p1 最终指向的是数组里下标为 1  那个 int 元素的起始地址。

所以,“p1是指针类型”,和 “p1指向的数据是什么类型” 是两回事。

知道这个以后,说结论:编译器它只认指针自己的 “身份证”(类型),不认指针指向的 “实际内容”。

比如:假设 p1 指向的内存地址是0x100,这段内存里存的二进制是 00000000 00000000 00000000 00001010(共 4 字节,对应十进制 10)

现在分析这个事:

如果,用 p1 本身的类型char* 解读,编译器看到char*,就只会 “抓 1 字节” 来读 —— 电脑都是小端序,因为都是最低位字节开始先运算,比如 13 + 47 都是先 3 + 7 ,那小端序的低字节存在低地址运算就很方便,低地址先拿到低字节在对,所以,假设这段 4 字节 int 数据(值为 10,二进制00000000 00000000 00000000 00001010),存在内存地址 0x100~0x103 里。按「小端序」存储时,字节会反过来放:

      • 地址0x100(低地址):存最低位字节 00001010(对应十进制 10)

      • 地址0x101:存 00000000

      • 地址0x102:存 00000000

      • 地址0x103(高地址):存最高

只看0x100地址开头的 1 字节:00001010,解读成char类型的数值 10(或 ASCII 码对应的控制字符)。

如果,强制转换成 int * 类型解读,当我们写*(int*)p1时,是先把 p1 的 “身份证” 临时改成int*(告诉编译器:“别当 char 看了,按 int 读!”)。编译器看到int*,就会 “抓 4 字节” 来读 —— 把0x100开始的 4 字节00000000 00000000 00000000 00001010完整读出来,解读成int类型的数值 10。

代码里虽然没有直接写p1 + 1,但核心逻辑依赖char*字节级偏移特性char* p1 = (char*)arr + j * elementSize;,这里的(char*)arr将数组地址转为char*后,每次计算偏移时:

elementSize4int的大小),则j*4就是按字节偏移,精准定位第jint元素

elementSize8char*的大小),则j*8精准定位第j个字符串指针

如果p1int*int* p1 = (int*)arr + j; 会自动按int大小偏移(等价于j * sizeof(int)),但这样就无法通用(比如处理char*数组时,偏移量计算会错误)。而char*的作用是强制按 1 字节为单位计算偏移,配合elementSize实现 “任意类型元素的地址定位”。

它指向的数据的类型,取决于数组元素(可能是intchar*等),这里在mySort中,并不需要直接解读数据(交给compare函数处理),只需要定位元素地址,然后用char*来移动具体的 int 的 4 字节,还是char 的 1 字节

p1指向int元素时,p1的类型还是char*(指针类型),但它指向的数据是int类型;

p1指向char*元素时,p1的类型依然是char*(指针类型),但它指向的数据是char*类型(另一个指针)。

再说点别的,有助于理解这个代码里的指针,因为好多细节专业术语妈逼的搞不透彻追问豆包总有歧义:

*(int*)a 是将 void* 类型的指针 a 先转换为 int* 类型指针,再解引用获取其指向的 int 类型值。

分析步骤:

(int*)a:将通用指针 avoid* 类型)强制转换为指向 int 类型的指针(int* 类型)

*(int*)a:对转换后的 int* 指针进行解引用,获取该指针指向的内存中存储的 int 类型数据

继续:

p1的类型是int*,那p1 + 1是移动 4 个字节,

p1的类型是char*,那p1 + 1是移动 1 个字节,

p1指向的数据类型与“移动多少字节”无关,只决定解读内存时按什么类型解析(如int占 4 字节、char占 1 字节)。

所以,移动字节数仅由指针自身类型决定,与指向的数据类型无关。

解读内存时按什么类型解析”指的是:当通过指针访问其指向的内存数据时,编译器会根据指针的类型来确定如何 “翻译” 这段内存中的二进制数据。例如:

若指针是int*类型:访问时会将指针指向的 4 字节内存数据解读为一个int整数(比如 0x0000000A 会被解读为 10,因为 0x 是十六进制,)。

若指针是char*类型:访问时会将指针指向的 1 字节内存数据解读为一个char字符(比如 0x41 会被解读为 'A')。

同样的一段内存(比如二进制 0x00000041),用int*解读是整数 65,用char*解读是字符 'A'

字符  'A' 和整数 65 数值相等,但语义完全不同 —— 一个是字符,一个是数字。

因为:

0x41 是十进制的 65,二进制是 0x01000001,

0xA 是十进制的 10,二进制是 1010。

一点一点说,当小说看就行, 都是我追问好久才明白的,这些很多之前就懂,但妈逼的之前人都是东一句西一句说东西妈逼的一点都没衔接、没体系,导致我一直知道的东西其实都是零散的,整合到一起就发现导出都是矛盾的!现在刚梳理精通!

首先比如数据: 0x00 0x00 0x00 0x41 ,按照大端序存储,即字节地址从低到高排列为:第 1 字节0x00、第 2 字节0x00、第 3 字节0x00、第 4 字节0x41

注意这里有个东西要说的就是,“地址”和“数据”是两码事,这里数据是0x00000041,按照大小端存于内存,那存到了哪里?比如存到了0x100开始的地址,那具体情形就是:

image

所以注意看,“地址”和“存储的数据”都是十六进制,所以要区分开!我一开始是看到0x某某某就错误地以为都是是地址,但其实我们日常,因为 1 字节 = 2 位十六进制,位数少,说着方便,就都用0x的十六进制形式表示“地址”和“数据”,而计算机里全都是二进制存的。

所以存的时候都是二进制 8 位!哪怕0也要存成是 00000000,不会省略任何前导 0!但由于人们表示会有2、8、10、16进制,这是数值的表示层面也是人们书写的时候,即书写形式,实际存储固定写死就是 8 位二进制没任何商量!最前面的 0 也要写。(唉这些都是任何地方都没说清的,哎真的好累这样学习,感觉乌烟瘴气只会骗来骗去的职场,我这样真傻!尤其那些公众号教咋么面试,教各种话术面试官喜欢听什么,教各种现成的项目,导致妈逼的一群水货,然后标准越来越高)

那既然说清了这些,就知道了:

1 字节 = 8 位二进制 ,但一旦非二进制就无所谓位数了,

1 字节 = 1 ~ 3 位八进制

1 字节 = 1 ~ 3 位的十进制

1 字节 = 1 ~ 2 位十六进制

那继续说如果是int*的指针指向这个数据首地址:

第1字节:00000000(0x00)

第2字节:00000000(0x00)

第3字节:00000000(0x00)

第4字节:01000001(0x41)

合并后也就是读取的结果是:00000000 00000000 00000000 01000001,转换为十六进制:0x00000041。转换为十进制:65(计算:0x41 = 4×16 + 1 = 65,高位的0x00不影响结果)。最终结果:*((int*)ptr) = 65(作为int类型的数值 65)。

而如果是char*,只访问一个字节,仅读取指针指向的起始地址对应的第 1 个字节0x00,注意:若指针指向第 4 字节,则读取0x41,此处以指向第 1 字节为例)。

那就是 0,如果指向第四个字节,就是65,ASCII就是字符'A'。

理解后我们再继续:

那如果再以小端序为例,

int*指针访问,从内存里读的时候,内存从低地址到高地址:0x41、0x00、0x00、0x00,然后合并也就是真正要读结果的时候是:00000000 00000000 00000000 01000001(实际存储顺序相反)。十六进制:0x00000041,十进制:65。

char * 指针访问,仅读取指针指向的单个字节,若指向最低地址(存储 0x41 的位置):读取 0x41,十进制 65(对应 'A')。若指向其他地址(存储 0x00 的位置):读取 0x00,十进制 0。结果是样的。

到这也就真正理解了大小端:

那么有些描述不清楚的地方再次梳理清晰一点就是:大小端是由CPU架构决定的,内存只是按 CPU 要求的顺序存储字节,本身无 “端序” 属性。想表示一个数,内存里大小端存的是不同的。

要表示同一个数(如 0x01020304),内存存储的字节顺序完全不同:

大端 CPU 要求内存存:0x01(低地址)、0x02、0x03、0x04(高地址)【解读的时候,低地址存的是原数据的高字节,那高字节就是数据的首位,即 CPU 按照顺序合并得到原值】

小端 CPU 要求内存存:0x04(低地址)、0x03、0x02、0x01(高地址)【解读的时候,低地址存的是原数据的低字节,也就是末位,即 CPU 按逆序合并得到原值】

就这么简单点事,之前搞不懂其实是因为解释太简练,导致主语都不知道说的是啥,搞混了0x到底值地址还是数据、不知道解读内存存的东西咋理解?谁解读?咋解读?这些其实都是因为解释的时候太简练,现在追问清楚了。

再继续说些我认为【相当重要且不知道就始终无法理解的零零碎碎的细节】:

16 是 2 的 4 次方(2⁴=16),1 位十六进制数能表示 0~15 共 16 个值,而 4 位二进制数的取值范围也是 0000~1111(对应 0~15),两者能完全对应,所以 1 位十六进制固定对应 4 位。

一位十六进制对应几位二进制?是 4 位,实际计算机存也是 强制 4 位。但写的时候可以省略前面的 0 。

0x41 是十六进制,转换为二进制,十六进制的 “4” 对应 4 位二进制 “0100”,十六进制的 “1” 对应 4 位二进制 “0001”,按原顺序把这两组 4 位二进制拼起来,就是 “0100”+“0001”=“01000001”(这里的 “+” 是拼接,不是数值相加)

现在说说进制,之前一直都觉得很懂,但再细节追问发现好多盲区。

之前进制转换轻车熟路,但没考虑一些细节,比如这里是拼接,那为何二进制“111”转十进制是相加?即 4 + 2 +1?

二进制 ↔ 八进制:拼接

二进制 ↔ 十六进制:拼接

八进制 ↔ 十六进制:拼接

八进制转十六进制:先转二进制(1 位八进制→3 位二进制),再转十六进制(4 位二进制→1 位十六进制,不足补前导 0)

十六进制转八进制:先转二进制(1 位十六进制→4 位二进制(补前导 0 凑 4 位));再转八进制(二进制串从右往左每 3 位分组(补前导 0 凑 3 位),每组变成 1 位八进制)

八进制 / 二进制 / 十六进制 → 十进制:只能相加,比如 :32 → 3×8¹ + 2×8⁰ = 24+2=26 

八进制 / 二进制 / 十六进制 十进制:除基取余(除法逆推),比如:十进制 26→除以 2 取余得二进制 11010;除以 8 取余得八进制 32;除以 16 取余得十六进制 1A

至于为啥十进制这么特殊?比如八进制的 10 ,那转十进制是相加算得 8,转二进制拼接的话是 001 000。其实“拼接” 是 “特殊福利”,“相加” 才是 “通用规则”:

二进制、八进制、十六进制之所以能 “拼接”,是因为它们的基数是 2 的幂次(2¹=2,2³=8,2⁴=16)

1 位八进制 = 3 位二进制(因为 8=2³),每 1 位八进制数刚好能覆盖 3 位二进制的所有组合(000~111 对应 0~7);

1 位十六进制 = 4 位二进制(因为 16=2⁴),每 1 位十六进制数也刚好覆盖 4 位二进制的所有组合(0000~1111 对应 0~F)。

这种 “基数是 2 的幂次” 的巧合,让它们能按固定位数拆分 / 组合(拼接)。

十进制的基数是 10,10 不是 2 的任何整数次幂(2³=8、2⁴=16,中间没有 10),所以:

1 位十进制数(0~9),没法用固定位数的二进制完全对应(3 位二进制只能到 7,4 位二进制能到 15,都和 10 不匹配);

反过来,n 位二进制也没法刚好对应 1 位十进制。只能用所有进制通用的 “按位权计算” 逻辑 —— 也就是 “每一位数字 × 位权,再相加”。

再比如,二进制 101100 转八进制:

      • 按 “拼接”:拆成 101 和 100(3 位一组),对应八进制 5 和 4,结果是八进制 54;
      • 按 “位权展开相加”:先算二进制 101100 的十进制 = 1×2⁵+0×2⁴+1×2³+1×2²+0×2¹+0×2⁰=32+0+8+4+0+0=44;再把十进制 44 转八进制(44÷8=5 余 4),结果也是八进制 54。

这个拼接的福利基于: 8=2³、16=2⁴,刚好能和二进制按固定位数对应,才省了 “先转十进制” 的步骤,本质还是基于 “位权” 的逻辑,不是脱离 “次方 / 位权” 的新方法。

之前觉得好像十六进制都是 4 位,但其实 0x 开头的十六进制位数无限制。只是平常接触场景是 1 字节 = 8 位二进制 = 2 位十六进制

而且一般都不写前导 0 ,直接写有效数字部分。

零零碎碎说的这么多后,开说用 int*char*指针解读的差异(感觉这才是大小端的精华,相当重要的,不仔细思考的根本想不到这些):

假设内存 4 字节是:0x41 0x01 0x00 0x00(十六进制),小端序解读,

int* 解读:读 4 字节→0x00000141(十六进制)→ 转十进制是 321(整数);

char* 解读:只读 1 字节→0x41→ASCII 对应 'A'(字符)。哪怕连数值都不等。

我疑惑是,为啥不按照端序来,char*先读最后的00呢?

因为端序只影响多字节数据的拼接,不影响单字节数据的读取,

假设这 4 字节的内存地址是 低地址→高地址 排列:

0x100(地址)存 0x41

0x101(地址)存 0x01

0x102(地址)存 0x00

0x103(地址)存 0x00

内存地址永远是 “低地址在前,高地址在后”,这是硬件规则,和端序无关,再看 “不同指针的解读逻辑”为什么端序对 char* 没用?

*int 解读(多字节,端序生效):

int 占 4 字节,int* 会从指向的起始地址(比如 0x100)读取连续 4 字节,再按 “端序规则拼接成完整 int”。
小端序的规则是 “低地址存低字节,高地址存高字节”,所以 4 字节 0x41(低字节,地址 0x100)、0x010x000x00(高字节,地址 0x103),拼接后是 0x00000141(高字节在前,低字节在后的完整数值),对应十进制 321。

这里 “小端” 的作用是 “决定 4 字节怎么拼”,不是 “决定先读哪个地址”—— 地址还是从低到高读(0x100 → 0x103),但拼的时候把低地址的字节放数值的低位。

*char 解读(单字节,端序无效):

char 只占 1 字节,char* 只会从指向的起始地址(比如 0x100)读取 这 1 个字节,不会读其他字节。不管是大端还是小端,单字节数据不需要 “拼接”,所以端序规则用不上。只要 char* 指向低地址 0x100,读出来的就是该地址存的 0x41,对应 ASCII 的 'A'。

我之前 confusion 的点 “小端读就是先读最后地址的 00” 是错的 —— 端序不改变 “从低地址到高地址读数据” 的顺序,只改变 “读出来的多字节怎么拼”

总结:char* 读单字节,只看指向的地址,和端序无关;int* 读 4 字节,才需要按小端规则拼接,这俩逻辑不冲突。

补充下: 

0x00000041 本身只是十六进制数,是地址还是数据,看它在代码 / 场景中的角色:

    • 若写 char* p = (char*)0x00000041;,这里 0x00000041 是内存地址(指针 p 指向这个地址);

    • 若写 char c = 0x41; 或内存中某地址存着 0x00000041(4 字节),这里 0x00000041 是数据。0x00000041 由 4 个十六进制数字对(00、00、00、41)组成,每个对对应 1 字节,共 4 字节,体现在内存中占据 4 个连续存储单元
      • char c = 0x41; 等价于 char c = 'A';,0x41 是表示字符 'A' 的 ASCII 码值(十进制 65),以十六进制形式写的常量,char 类型变量能存 1 字节数据,0x41 刚好是 1 字节,所以可以直接赋值,赋值后 c 就是字符 'A';之前说的 0x00000041,若用于 char c = 0x00000041;,本质是把 32 位的十六进制数(值 65)截断为 1 字节给 c,最终 c 还是 'A';若用于 int* p = (int*)0x00000041;,才是把它当内存地址

核心区分:带指针符号(*)或用于赋值给指针变量时,它是地址;用于赋值给普通变量(char/int)或描述内存中存储的内容时,它是数据。

那说完上面这些零碎的基础,开始说点更进一步的实战,也通过这个实战,精通了数组究竟是咋回事,底层是咋存的,有了这个实战,才可谓是完美闭环

之前不懂啥叫 32 位数据,现在懂了。char* 读取的是指向的地址存储的字节,与大小端的关系如下,比如 32 位数据 0x00000041 :

    • 存在小端系统,首地址存 0x41 → char* 读 0x41

    • 存在大端系统,首地址存 0x00 → char* 读 0x00

核心是:char* 读的是 “指向地址的字节内容”,而内容由大小端决定。

然后再具体说下,char*读取的是指针指向的那个字节,与大小端无关,若指向首地址,大端存0x00就读0x00,小端存0x41就读0x41。大小端影响多字节数据的存储顺序,不改变char*读取单字节的规则。

说白了就是,char*指向数据首地址,但首地址存储的字节内容,由大小端决定:

大端:首地址存0x00

小端:首地址存0x41

所以也就透彻了char*不是跑去读首和尾啥的,他就是指啥读啥,只是指的由大小端确定好了。而内存咋存,是大端小端,由 CPU 结构规定的大小端?

然后我做了一些实验:

目前主流是小端,想按照小端存数的顺序输出,就通过 char* 逐个遍历:

      • 代码:

        查看代码
        #include <stdio.h>
        
        int main() {
            int num = 0x12345678; // 假设一个32位整数
            char *ptr = (char*)&num; // 用char*逐个访问字节
        
            printf("整数 0x%X 的小端存储字节(从低地址到高地址):\n", num);
            for (int i = 0; i < 4; i++) {
                printf("地址偏移 %d: 0x%02X\n", i, (unsigned char)ptr[i]);
            }
        }

        输出:

        image

char*输出能看到底层大小端存储的字节情况

int*输出的是做了加工合并后的完整值,看不出大小端。

小端电脑,模拟大端的输出:

      • 代码:

        查看代码
        #include <stdio.h>
        
        // 函数:判断当前系统是大端还是小端
        int isBigEndian() {
            union {
                int i;
                char c[4];
            } u;
            u.i = 0x01020304;
            // 大端:低地址存0x01,小端:低地址存0x04
            return (u.c[0] == 0x01);
        }
        
        // 函数:在小端系统中按大端规则重组32位整数
        unsigned int littleToBig32(unsigned int num) {
            unsigned char* bytes = (unsigned char*)&num;
            // 小端存储顺序:bytes[0](低地址)是最低位字节
            // 重组为大端:高位字节放前面
            return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
        }
        
        int main() {
            // 1. 查看当前系统主流存储方式
            if (isBigEndian()) {
                printf("当前系统是大端存储(高位字节存低地址)\n");
            } else {
                printf("当前系统是小端存储(低位字节存低地址)\n");
                printf("注:x86/x86_64架构(Intel/AMD)均为小端,是目前主流\n\n");
            }
        
            // 2. 小端系统模拟大端输出示例
            unsigned int num = 0x12345678; // 原始数值
            printf("原始数值:0x%08X\n", num);
        
            // 小端系统原生输出(直接解引用int*,与存储顺序无关)
            printf("小端系统原生输出(int*):0x%08X\n", num);
        
            // 模拟大端输出(手动重组字节)
            unsigned int bigEndianValue = littleToBig32(num);
            printf("小端模拟大端输出:0x%08X\n", bigEndianValue);
        
            // 3. 验证字节存储细节
            unsigned char* bytes = (unsigned char*)&num;
            printf("\n内存中实际存储的字节(低地址到高地址):\n");
            printf("偏移0:0x%02X\n", bytes[0]);
            printf("偏移1:0x%02X\n", bytes[1]);
            printf("偏移2:0x%02X\n", bytes[2]);
            printf("偏移3:0x%02X\n", bytes[3]);
        }
      • 代码解释:

代码一:return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];

num=0x12345678(小端存储,bytes [0]=0x78, bytes [1]=0x56, bytes [2]=0x34, bytes [3]=0x12):

        • bytes[3] << 24:0x12 → 00010010 → 左移 24 位 → 00010010 00000000 00000000 00000000

        • bytes[2] << 16:0x34 → 00110100 → 左移 16 位 → 00000000 00110100 00000000 00000000

        • bytes[1] << 8:0x56 → 01010110 → 左移 8 位 → 00000000 00000000 01010110 00000000

        • bytes[0]:0x78 → 01111000 → 保持 → 00000000 00000000 00000000 01111000

按位或后:00010010 00110100 01010110 01111000 → 0x12345678(大端序整数)

注意:低地址永远是内存地址的左边,从左往右,高低位永远是权重。

代码二union联合体,联合体uint ichar c[4]共享 4 字节内存。特性:所有成员共用同一块内存,大小等于最大成员,可以节省内存(同内存块存不同类型数据)、解析二进制数据(用不同成员看同一块内存)、硬件寄存器操作(按位 / 按字节访问同地址)。

以 0x01020304 这个 32 位整数为例,大端和小端系统中,int i 和 char c[4] 在联合体中的存储细节:

大端系统(高位字节存低地址)

内存布局(地址从低到高):

地址 0x1000 → 0x01(数据的最高位字节)

地址 0x1001 → 0x02

地址 0x1002 → 0x03

地址 0x1003 → 0x04(数据的最低位字节,最右侧的)

int i 的存储

i 作为 4 字节整数,整体值为 0x01020304,对应内存中 4 字节的排列就是上述顺序。

char c [4] 的存储

c 是字节数组,按地址顺序访问:

c[0](地址 0x1000)= 0x01

c[1](地址 0x1001)= 0x02

c[2](地址 0x1002)= 0x03

c[3](地址 0x1003)= 0x04

小端系统(低位字节存低地址)

内存布局(地址从低到高):

地址 0x1000 → 0x04(最低位字节)

地址 0x1001 → 0x03

地址 0x1002 → 0x02

地址 0x1003 → 0x01(最高位字节)

int i 的存储

i 作为 4 字节整数,整体值仍是 0x01020304(逻辑值不变),但内存中字节反转排列。

char c [4] 的存储

c 按地址顺序访问:

c[0](地址 0x1000)= 0x04

c[1](地址 0x1001)= 0x03

c[2](地址 0x1002)= 0x02

c[3](地址 0x1003)= 0x01

核心结论:无论大端 / 小端,int i 的逻辑值(0x01020304)不变,变的是 4 个字节在内存中的排列顺序char c[4] 作为字节数组,直接暴露了这种排列差异,通过 c[0] 的值就能区分大小端。int负责 “整体赋值”,char[4]负责 “拆分查看每个字节的位置”,缺一不可。

这也看出来了,int哪怕存的是倒序的,读的时候也会自动整合为逻辑数据的顺序,始终看不懂底层的大小端,而只有char才能看到底层的大小端。也可以验证,

        • 代码:

          查看代码
          #include <stdio.h>
          
          union EndianCheck {
              int num;
              char bytes[4];
          };
          
          int main() {
              union EndianCheck ec;
              ec.num = 0x12345678;  // 假设int占4字节
          
              // 输出每个字节的存储值
              printf("字节顺序: %02X %02X %02X %02X\n",
                     ec.bytes[0], ec.bytes[1], ec.bytes[2], ec.bytes[3]);
          
              // 判断大小端
              if (ec.bytes[0] == 0x78) {
                  printf("小端模式\n");  // 低地址存低字节(78)
              } else if (ec.bytes[0] == 0x12) {
                  printf("大端模式\n");  // 低地址存高字节(12)
              }
          }
        • 解释:

          0x%3X

          • 0x 表示在输出前加前缀 "0x"(十六进制标识)。

          • %3X 表示以十六进制大写形式输出,至少占 3 个字符宽度。不足 3 位,前补空格。

0x%03X

          • 同样带 "0x" 前缀。

          • %03X 表示以十六进制大写形式输出,至少占 3 个字符宽度。不足 3 位,前补 0 。

int*占位符 %d 就输出十进制,%x就输出十六进制,%o八进制

再说一点:

0x00有效数据字节,不是 “无效的”,内存里本就会存储0x00这类值,其次,指针 “指向哪个字节”,不是 “手动强制指向0x00”,而是指向存储0x00的那个内存地址—— 比如这 4 字节数据存在地址 X(存0x00)、X+1(存0x00)、X+2(存0x00)、X+3(存0x41),指针的指向是 “选择从哪个地址开始读”:

        • 若代码里让char*指针等于(char*)&int_dataint_data是这 4 字节的整数变量),它就指向 X(读0x00);
        • 若让char*指针等于(char*)&int_data + 3,它就指向 X+3(读0x41)。而一般指针指向都是最开头的首地址

而之前说的比如 005 和数字 5 是一样的,这里 00 是无效的,但内存里内存地址就是实实在在存的 00,那就是有效数据,

比如 0x0041 作为 16 位数据,若用 char*指向 16 位的 0x0041,解引用读取低地址字节,小端模式下是 0x41(65),大端模式下是 0x00(0);

比如 0x41 作为 8 位数据,char*读取始终是 0x41(65)。

0x00410x41int* 读取都是 65。

      • 解引用的方法:

        查看代码
        short val = 0x0041;  
        char* p = (char*)&val; // val 为 16 位的 0x0041,p 指向其低地址
        char c = *p; // 解引用获取低地址字节
        printf("c的十六进制值: 0x%02X,对应的字符: %c\n", (unsigned char)c, c);
        
        //小端模式下输出:c的十六进制值: 0x41,对应的字符: A
        //大端模式下输出:c的十六进制值: 0x00,对应的字符: (空字符)

Q:地址宽度?

A:本质是CPU用来 “定位内存位置” 的 “二进制编号的总位数”。

地址宽度不是软件随便定的,而是CPU 和内存之间的 “连接线(地址总线)” 数量决定的—— 这是硬件出厂时就焊死的规则:每一条 “地址总线” 只能传输 1 位二进制数(要么 0,要么 1);

32  / 64 位地址宽度 → 对应 32 / 64 条地址总线(能同时传 32 / 64 位二进制数);

可以表示最大数是 232 = 4GB 或者 264

0x这种十六进制的表达,0x100 是 3 位十六进制,1 位十六进制对应 4 位二进制

那就是 12 位二进制:0001  0000  0000 

在 64 位系统中的就是:00000000 00000000 00000000 00000000 00000000 00000000 00000001 00000000。这就是地址总线和位数的关系

32 位写死固定地址宽度就是 32 位,4 字节

64 位写死固定地址宽度就是 64 位,8 字节

Q:上面说了地址,那再说下“地址”和“数值”有何不同?

A:在 64 位系统中存储时,

若定义为long long类型(64 位系统中默认 8 字节的整数类型):存储时同样补前导 0 到 64bit,占 8 字节;

若定义为int类型(64 位系统中多数仍为 4 字节):则补前导 0 到 32bit,占 4 字节 —— 但这是 “数据存储”,和 “地址存储” 的 8 字节规则无关,地址在 64 位系统中永远是 8 字节,不受数据类型影响。

总结:64 位的话,地址的存储宽度:固定 8 字节(因 64bit=8 字节,是 64 位系统的硬件规则);

地址 0x100 的存储:二进制补前导 0 到 64bit,实际占 8 字节;

和 32 位系统的区别:仅在于地址存储宽度(32 位 4 字节,64 位 8 字节),但 64 位系统是当前个人笔记本的绝对主流,地址存储只看 8 字节规则即可。

无论是 int 还是 long 都是补的前导 0 位数不同,但实际表示的都是 256 这个数,具体计算过程:

十六进制数每位的权重是 16 的幂,从右往左依次是 16⁰、16¹、16²……

0x100 是 3 位十六进制数,从右到左各位数字依次是:第 0 位是 0,第 1 位是 0,第 2 位是 1。任何进制中都是右侧是第 0 位。

第 2 位(最左):1 × 16² = 1 × 256 = 256
第 1 位(中间):0 × 16¹ = 0 × 16 = 0
第 0 位(最右):0 × 16⁰ = 0 × 1 = 0

至此,小知识点完结

第二次体验到“知道一点就明白,再知道一点反而糊涂,再知道一点就完全懂了修正最开始的假明白”。

插入:

另外再说下之前 acm 金牌西交栗子保研到南京大学,鄙视考研的都在抄书上代码, 发朋友圈阴阳怪气考研的说“妈妈快看,十进制真的有 10 诶”

现在重新理解这个问题,十进制其实确实有十,但这是结论,单独拿出来很容易引起歧义,严谨的说, 十进制只有 0 ~ 9,在往上就是 10,我一直以为是单纯的叫“一零”,但就叫十,

再比如:

      • 二进制满 2 进 1,单个位符号是 0、1(无 2);

      • 十六进制满 16 进 1,单个位符号是 0-9、A-F(无 16);

      • 十进制满 10 进 1,单个位符号是 0-9(无单独的 “10” 符号)—— “10” 是十进制的两位组合,代表 “1 个 10 + 0 个 1”,本质是进位后的表示,和二进制的 “10”(代表 2)、十六进制的 “10”(代表 16)逻辑完全一致,并非十进制有 “16”“2” 那种单独的超范围符号。

对我而言读法只是方便沟通便于理解,过多的较真没意义,所以说下符号“10”的二进制、十进制、十六进制分别咋读:

      • 二进制:10 读作“二进制一零,十进制 2”

      • 十进制:10 表示比 9 多 1 的数值,就读作十,“一零”是单纯念符号,未体现十进制的位值逻辑

      • 十六进制:10 读作“十六进制一零”、“0x一零”、“十进制 16”

再讲白一点,A 是 10,10 是 16,两者差了 6 个数值,完全是两回事。

image

所以学通了就可以大胆的说,十六进制有 16(表示为0x10),二进制有 2,十进制有 10,这么想“妈逼的一个进制连数都表示不全,咋可能?”,关键是单个位可以表示最大数,那确实没有2、10、16

基数指某一进制中允许使用的不同数字符号的总数,也是该进制 “逢几进一” 的那个“几”:

      • 十进制基数 = 10:用 0-9 共 10 个数字,计数逢 10 进 1(如 19+1=20);

      • 二进制基数 = 2:用 0、1 共 2 个数字,计数逢 2 进 1(如 11+1=100);

      • 十六进制基数 = 16:用 0-9、A-F(共 16 个符号),计数逢 16 进 1(如 0xF+1=0x10);

再多逼逼几句其他的,不然妈逼的这种抽象的玩意没法和实际对应,总忘:

64 位系统需 64 根实际的物理地址线,即电线,很微小,继承在CPU上,那 64 根电线用 0、1 来控制出 264 种状态,也是用于定位内存单元的数字编码,每种状态 (每个编码)就叫一个地址,对应的就是内存虚拟分割的最小单元。(只是很多用不到,所以 64 位系统实际 48 根电线)

再深入抽插(如果不知道上面这些还好,一旦了解这些反而觉得很乱,因为我内心还有下面的思考疑惑,但现在疑惑解开后,豁然开朗了,直接精通咔咔咔咔!):

32 位系统说,总共是 232 个地址编号,每个地址编号绑定 1 字节的内存单元,所以 32 位系统总共 42 亿字节,也即是 4GB,再多逼逼下,B 是字节,G 是吉,GB 是吉字节,所以说 4GB = 4G 字节 = 4 吉字节,

至此也明白了为啥一会说“【一个地址是一字节空间】”,一会又说“【地址他妈的是64位系统是8字节,32位系统是4字节】”,内心 OS:妈逼的地址到底几字节??

现在捋顺完真他妈透彻艹,但费精力!写了一周

这都是我一点一点追问出来的,期间豆包还反复说错误的东西,逐步碎片化捋顺,然后质疑豆包,反复打磨思考追问,最后形成一个正确的知识,好鸡巴坎坷艹

自己探索学习比看人家写好多了,之前小林那估计也看过但完全没印象,自己思考摸索推进追问知识才深刻!!书读百遍其义自见

第一次体验到“知道一点就明白,再知道一点反而糊涂,再知道一点就完全懂了修正最开始的假明白”

说了这么多就是捋清楚各种术语,就跟仪仗队一开始只站军姿一样,现在透彻了,开始说这个写法三的代码,妈逼的铺垫的知识,边学边写博客搞了俩星期,现在再看这些针对写法三代码的解释就相当开门透彻理解了:

逐处解释带*的部分和void的作用:

    • 1. 比较函数compareInt
      int compareInt(const void* a, const void* b) {
          return *(int*)a - *(int*)b;
      }

void* avoid*是 "无类型指针",可以指向任何类型的数据,但不能直接解引用(因为不知道指向的数据大小)。这里作为参数,是为了让函数能接收任意类型的指针(实现通用性)。

const void* aconst表示指针指向的数据不可修改,保证比较时不会意外修改原数据。

(int*)a:将void*强制转换为int*(整数指针),明确指向 "int 类型数据",此时指针知道要访问 4 字节(假设 int 占 4 字节)。

*(int*)a:对转换后的int*解引用,获取指针指向的 int 类型数值。

void* a:同上,无类型指针,接收任意类型数据的地址(这里实际接收的是 "字符串指针的地址")。

(char**)a:将void*转换为char**("指向字符串指针的指针")。因为排序的是char* strArr[](字符串指针数组),数组中每个元素是char*(字符串指针),所以a实际指向的是char*类型的元素,因此需要两层指针。

*(char**)a:解引用char**,得到char*(字符串的地址),传给strcmp比较字符串内容。

void* arr:无类型指针,指向要排序的数组。因为数组可能是 int 数组、字符串指针数组等任意类型,用void*可以兼容所有类型。

int (*compare)(const void*, const void*):函数指针,指向 " 接收两个const void*参数、返回 int 的函数 "。用于接收不同类型的比较函数(如compareIntcompareString),实现根据数据类型动态选择比较逻辑。

char* p1:将void* arr转换为char*(字符指针),因为char*每次移动 1 字节,方便按 " 元素大小elementSize" 计算下一个元素的地址(如j * elementSize就是第 j 个元素相对于数组开头的偏移量)。

(char*)arr + j * elementSize:通过字符指针的算术运算,定位到数组中第 j 个元素的地址(无论元素是 int、char * 还是其他类型,都能准确定位)。

void*的作用:作为 "通用指针",打破类型限制,让函数能接收 / 处理任意类型的数据(数组、不同类型的元素等)。

*的指针转换(如(int*)achar**):void*无法直接使用,必须转换为具体类型的指针才能解引用或进行类型相关的操作,转换时需要匹配实际指向的数据类型(否则会出错)。

函数指针(*compare):让通用排序函数能根据不同数据类型,调用对应的比较逻辑,实现 "一套排序代码处理所有类型" 的泛型效果。

各个函数里的强制转int*是为了读4字节,而这里p1只是个索引,用char*是为了通过elementSize计算偏移(j * elementSize)能精确定位到任意类型元素的起始地址,无论元素是int(4 字节)还是char*(8 字节),都能精确移动到下一个元素。而接下来的compare去调用实际的是真正的比较数据,会根据实际数据类型(如intchar*)进行指针转换和解引用,提取真实数据进行比较(如*(int*)a取整数,*(char**)a取字符串地址),实现对应类型的比较逻辑。

char* strArr[] = { "cat", "apple", "banana" };每个元素都是char*(字符串指针),指向对应字符串的首地址。如strArr[0]是指向 "cat" 首字符 'c' 的指针,strArr[1]是指向 "apple" 首字符 'a' 的指针。

mysortcompare接收所有参数和返回值是这个样子的函数,然后main里的mysort传实际的。

Q:狗逼玩意为啥刷题没用过指针,现在学C++不用指针好像活不了一样?

A:因为指针是 C 语言里 “处理未知类型、实现通用功能” 的唯一手段。刷的算法题场景固定、类型单一,自然用不到;但一旦要写能复用的通用代码(比如库函数),就绕不开

就这一段搞了三周艹!~~~~(>_<)~~~~以上全部内容都是自己追问豆包得来的