当前位置: 首页 > news >正文

C/C++语言知识点一

目录

1. 请对这段代码进行解释:char *const *(*next)( );

2. 函数指针数组:解释这个表达式char *(*c[10])(int **p); 

3. 字符串常量:分析下面这段代码。 

4. 访问指定内存地址

5. typedef 和 define 的区别

6. 函数返回局部变量地址问题

7. 无符号整数和有符号整数相加问题 

8. 大端模式和小端模式如何用代码判断

9. 显示无符号int类型的最大值和最小值

10. 逻辑运算符 (&&) 和 位运算符 (&)

11. printf函数的返回值 

12. 通过#运算符,利用宏参数来创建字符串

13. “##” 运算符的作用

14. 结构体中使用字符数组还是字符指针

15. 内存越界问题 

16. free 和 delete 是如何处理指针


1. 请对这段代码进行解释:char *const *(*next)( );

next是一个函数指针,指向一个没有参数的函数,并且该函数的返回值是一个指针,该指针指向一个类型为char的常量指针。

1.(*next)表示next是一个指针,指向某个函数,因为后面跟着(),所以这个指针指向一个函数。
2.next是一个函数指针,函数 (*next)() 的返回类型是 char *const * :
    最外层 *:返回值是一个指针
    const:该指针是常量(指针本身不可修改)
    char *:这个常量指针指向一个 char 类型的指针   
3.char *const,这表示一个指向char的指针,而且这个指针是const的,也就是说,它指向的地址不能被修改。

讨论:我认为,char * const *(*next)( )指的是,返回值为char * const *(这是一个char类型的二级指针)的函数指针,我们把p代指这个二级指针,这个p具有的特点为,p的值可以改变,*p的值不能改变,**p的值可以改变,因为const的原理是它后面修饰的变量不能改变,而在p中const后面只有一个*,所以仅*p不能改变其值。


2. 函数指针数组:解释这个表达式char *(*c[10])(int **p); 

c 是一个数组,包含 10 个函数指针,每个函数指针指向的函数接受一个 int ** 类型的参数,并返回一个 char * 类型的结果。

char *(*c[10])(int **p);
分析:
    c 是一个数组,包含10个元素,每个元素是函数指针;
    *c 表示数组的每个元素都是指针;
    (*c) 后的 (int **p) 表示这些指针指向函数,且函数接受一个 int** (指向整型指针的指针)类型的参数;
    最外层的 char * 表示这些函数返回 字符指针(char*)。

螺旋法则(Clockwise/Spiral Rule)详解:

螺旋法则是 解析C语言复杂声明 的核心方法,由计算机科学家 David Anderson 提出。它通过从变量名出发,按顺时针方向(螺旋式)解析修饰符,帮助开发者直观理解多层指针、数组和函数组合的声明。


解析步骤(四步法则):

  1. 起点:从变量名(标识符)开始
  2. 方向:按顺时针方向(优先向右,无法继续时向左)解析修饰符
  3. 优先级() > [] > *
  4. 终止:处理完所有修饰符或遇到结束符 ;

3. 字符串常量:分析下面这段代码。 

char *s = "AAA";
printf("%s", s);
s[0] = 'B';
printf("%s", s);

解析:s[0] = 'B'; 这行代码试图修改字符串常量的第一个字符,这是不安全的,因为字符串常量通常存储在只读内存区域。


4. 访问指定内存地址

问题:嵌入式系统经常需要程序员访问特定的内存位置,读取或写入新的值,尤其是在嵌入式处理器开发中操作寄存器时。例如,在某个项目中,需要设置一个绝对内存地址为0x40020800的位置,并将该地址的内容设置为整数值0x3456。现在需要编写代码来完成这个任务。 

考点:访问地址时,强制类型转换,强制为一个整形数,该操作是合法 的。

#include <stdint.h>  // 包含标准整型定义

// 方法1:直接指针操作
*(volatile uint32_t *)0x40020800 = 0x3456;

// 方法2:宏定义(推荐)
#define REGISTER_ADDR ((volatile uint32_t *)0x40020800)
*REGISTER_ADDR = 0x3456;

// 方法3:通过结构体映射(适用于多寄存器场景)
typedef struct {
    volatile uint32_t reg;
} DeviceReg;

DeviceReg *const device = (DeviceReg *)0x40020800;
device->reg = 0x3456;

// 方法4:创建一个指向特定地址的指针
volatile uint32_t *reg = (uint32_t *)0x40020800;
*reg = 0x3456;

注意:
1. 是否需要使用volatile关键字?是的,因为访问硬件寄存器必须使用volatile,防止编译器优化掉写入操作,或者重复读取时使用缓存的值。
2. uint32_t 和 unsigned int 的主要区别在于大小的固定性。uint32_t 始终是32位,而 unsigned int 的大小可能会因平台而异。在跨平台编程、嵌入式系统或处理网络协议和文件格式时,使用 uint32_t 可以确保数据类型的精确匹配,避免溢出或截断问题。


5. typedef 和 define 的区别

5.1 功能与行为 

(1)#define 是预处理指令,在编译前进行文本替换。
        #define INT_PTR int *
        INT_PTR a, b;  // 展开为 int *a, b; (b 是 int 类型,非指针)
(2)typedef 是类型别名定义,由编译器处理,创建新的类型名称。
        typedef int *INT_PTR;
        INT_PTR a, b;  // a 和 b 都是 int* 类型

5.2 作用域

(1)#define 从定义点开始,到文件末尾或 #undef 取消定义为止。无块作用域,不可在局部范围内定义。

void func() {
    #define LOCAL_MACRO 10  // 宏定义作用域为整个文件
}

(2)typedef 支持块作用域,可在局部范围内定义。

void func() {
    typedef int MyInt;  // 类型别名作用域为 func 函数内
    MyInt a = 10;
}

5.3 类型检查

#define 无类型检查,直接替换文本,可能导致意外错误。

typedef 由编译器处理,支持类型检查,语义更明确。

5.4 复杂类型支持

#define 需要额外括号和小心使用,否则可能出错。

#define FUNC_PTR int (*)(int)
FUNC_PTR f1, f2;  // 展开为 int (*f1)(int), f2; (f2 是 int 类型,非函数指针)

typedef 直接支持复杂类型,语法更清晰。

typedef int (*FUNC_PTR)(int);
FUNC_PTR f1, f2;  // f1 和 f2 都是函数指针类型

5.5 与 const 结合

#define 宏替换可能导致意外结果。

#define PTR int *
const PTR a = NULL;  // 展开为 const int *a = NULL; (a 是指向常量的指针,非常量指针)

 typedef 语义明确,与 const 结合行为可预测。

typedef int *PTR;
const PTR a = NULL;  // a 是常量指针,类型为 int *const

5.6 使用场景对比

适合使用 #define 的场景: 

// 1.定义常量:
#define PI 3.14159

// 2.条件编译:
#define DEBUG
#ifdef DEBUG
printf("Debug mode\n");
#endif

// 3.简单文本替换:
#define MAX(a, b) ((a) > (b) ? (a) : (b))

 适合使用 typedef 的场景:

// 1.定义类型别名:
typedef unsigned char uint8_t;

// 2.简化复杂类型:
typedef int (*FuncPtr)(int, int);

// 3.提高代码可读性:
typedef struct {
    int x;
    int y;
} Point;

6. 函数返回局部变量地址问题

问题:请指出下面这段代码的错误。 

#include <stdio.h>

char *get_str(void);

int main(void)
{
    char *p = get_str();
    printf("%s\n", p);
    return 0;
}

char *get_str(void)
{
    char str[] = {"abcd"};
    return str;
}

问题分析

  1. 局部变量的生命周期

    • get_str 函数中,str 是一个 局部数组,其内存分配在栈上。

    • 当 get_str 函数返回时,str 的内存会被释放,返回的指针指向 无效内存

  2. 未定义行为

    • 在 main 函数中,p 指向了 get_str 返回的地址,但此时该地址的内容已经无效。

    • 访问 p 会导致 未定义行为(程序可能崩溃、输出乱码或看似正常但存在隐患)。

如何修改: 

方案1:返回静态局部变量(不推荐)

str 声明为 static,使其生命周期延长到程序结束:

char *get_str(void)
{
    static char str[] = {"abcd"};  // 静态局部变量
    return str;
}
  • 优点:简单直接,程序可以正确运行。
  • 缺点:静态局部变量在程序运行期间始终占用内存,且多线程环境下不安全。

方案2:返回字符串常量(推荐)

直接返回字符串常量,常量存储在只读内存中,生命周期为整个程序:

char *get_str(void)
{
    return "abcd";  // 字符串常量
}
  • 优点:简单高效,内存安全。
  • 缺点:返回的字符串不可修改(只读)。

方案3:返回字符串常量的地址

#include <stdio.h>

char *get_str(void);

int main(void)
{
    char *p = get_str();
    printf("%s\n", p);  // 输出 "abcd"
    return 0;
}

char *get_str(void)
{
    char *str = {"abcd"};  // 指针指向字符串常量
    return str;
}
  • 优点:程序可以正确运行,不会出现未定义行为。
  • 缺点:返回的字符串不可修改(只读)。但是,这种用法仍然存在问题,因为它违反了函数的封装性,即函数的实现细节(如返回字符串常量的指针)应该对调用者隐藏。

方案4:动态分配内存(推荐)

使用 malloc 动态分配内存,返回堆上的地址:

#include <stdio.h>
#include <stdlib.h> // 引入stdlib.h以使用malloc和free

char *get_str(void);

int main(void)
{
    char *p = get_str();
    if (p != NULL) {
        printf("%s\n", p);
        free(p);  // 释放内存
    }
    return 0;
}

char *get_str(void)
{
    char *str = (char *)malloc(5 * sizeof(char)); // 动态分配内存
    if (str == NULL) {
        return NULL; // 如果内存分配失败,返回NULL
    }
    strcpy(str, "abcd"); // 复制字符串到动态分配的内存
    return str;
}
  • 优点:返回的字符串可修改,内存管理灵活。
  • 缺点:需要手动释放内存,否则会导致内存泄漏。

7. 无符号整数和有符号整数相加问题 

段代码的目的是检查两个整数相加的结果是否大于6,并根据结果输出相应的字符串。 

#include <stdio.h>

int main(void)
{
    unsigned int a = 6;  // 无符号整数
    int b = -20;         // 有符号整数

    (a + b > 6) ? puts(">6") : puts("<=6");

    return 0;
}

注意:
1.在 C 语言中,当无符号整数和有符号整数混合运算时,有符号整数会 隐式转换 为无符号整数。
2.在计算机中,整数通常以补码的形式存储。 
3.负数的补码

  • a + b 的实际计算过程:
    • a 是无符号整数,值为 6
    • b 被转换为无符号整数,其值为 UINT_MAX - 19(假设 unsigned int 是 32 位,则值为 4294967276)。
    • 因此,a + b 的值为 6 + 4294967276 = 4294967282


8. 大端模式和小端模式如何用代码判断

  • 大端模式:数据的 高位字节 存储在内存的 低地址 处。
  • 小端模式:数据的 低位字节 存储在内存的 低地址 处。

方法1:通过检查一个多字节数据(如 int)在内存中的存储方式,可以判断当前系统的字节序。

#include <stdio.h>

int is_little_endian() {
    int num = 1;                  // 定义一个整数
    char *p = (char *)&num;       // 将整数的地址转换为 char*
    return *p == 1;               // 检查第一个字节是否为 1
}

int main() {
    if (is_little_endian()) {
        printf("小端模式\n");
    } else {
        printf("大端模式\n");
    }
    return 0;
}

方法二:使用联合体判断。联合体的特性是 所有成员共享同一块内存,因此可以通过联合体访问同一数据的不同字节。

#include <stdio.h>

int is_little_endian() {
    union {
        int num;      // 多字节类型
        char c;       // 单字节类型
    } u;
    u.num = 1;        // 将联合体的 int 成员设为 1,其二进制表示为 0x00000001
    return u.c == 1;  // 检查 char 成员是否为 1
}

int main() {
    if (is_little_endian()) {
        printf("小端模式\n");
    } else {
        printf("大端模式\n");
    }
    return 0;
}

9. 显示无符号int类型的最大值和最小值

为了正确地获取当前系统下 unsigned int 类型的最大值,可以使用 UINT_MAX 宏,这个宏定义在 <limits.h>limits 头文件中: 

#include <stdio.h>
#include <limits.h>

int main() {
    unsigned int zero = 0;
    unsigned int max_value = UINT_MAX;
    printf("The value of 0 for unsigned int: %u\n", zero);
    printf("The maximum value for unsigned int: %u\n", max_value);
    return 0;
}

 注意:也可以直接对0取反操作。

unsigned int zero = 0;
unsigned int max_value = ~zero; // 对0取反,得到无符号整数的最大值

10. 逻辑运算符 (&&)位运算符 (&)

关键区别:

  1. 短路求值
    • && 会短路,避免不必要的计算和潜在的错误。
    • & 不会短路,总是计算所有条件。
  2. 安全性
    • 使用 && 是安全的,因为它可以防止数组越界访问。
    • 使用 & 是危险的,可能会导致程序崩溃或未定义行为。

11. printf函数的返回值 

代码分析:答案:4321

#include <stdio.h>

int main() {
    int i = 43;
    printf("%d\n", printf("%d", printf("%d", i)));
    return 0;
}

总结:

  • printf 的返回值是打印的字符数。
  • 嵌套的 printf 调用会从内到外依次执行。
  • 最终的输出是 4321,包括换行符。

12. 通过#运算符,利用宏参数来创建字符串

        在 C 语言中,# 运算符是 字符串化运算符,它可以将宏参数转换为字符串字面量。通过结合宏定义和 # 运算符,可以轻松地将宏参数转换为字符串。

 代码分析:

#include <stdio.h>

// 定义宏
#define SQUARE(x) (printf(""#x" square is: %d\n", (x) * (x)))

int main() {
    int num = 5;

    SQUARE(num);
    SQUARE(3 + 2);
    SQUARE(10);

    return 0;
}

输出:

num square is: 25
3 + 2 square is: 25
10 square is: 100

解释:

  1. SQUARE(num)

    • #x 被替换为 "num"
    • (x) * (x) 计算为 5 * 5 = 25
    • 输出:num square is: 25
  2. SQUARE(3 + 2)

    • #x 被替换为 "3 + 2"
    • (x) * (x) 计算为 5 * 5 = 25
    • 输出:3 + 2 square is: 25
  3. SQUARE(10)

    • #x 被替换为 "10"
    • (x) * (x) 计算为 10 * 10 = 100
    • 输出:10 square is: 100

总结:

  • 这个宏定义通过 # 运算符将参数 x 转换为字符串,方便在输出中显示参数的具体形式。
  • 同时,宏会计算参数的平方值并输出。
  • 这种技巧在调试或需要显示表达式及其结果时非常有用。

13. “##” 运算符的作用

        在 C 语言中,##令牌粘贴运算符(Token Pasting Operator),用于将两个令牌(tokens)连接成一个新的令牌。它在宏定义中非常有用,特别是在需要动态生成标识符或代码时。

基本用法: 

#include <stdio.h>

// 定义宏,将两个令牌连接
#define CONCAT(a, b) a##b

int main() {
    int var1 = 10;
    int var2 = 20;

    // 使用宏连接令牌
    printf("%d\n", CONCAT(var, 1)); // 输出 var1 的值
    printf("%d\n", CONCAT(var, 2)); // 输出 var2 的值

    return 0;
}

 进阶用法:动态生成代码。## 运算符可以用于动态生成变量名、函数名或代码片段。

#include <stdio.h>

// 定义宏,动态生成函数名
#define CALL_FUNCTION(func_name, arg) func_name##_version_##arg()

// 定义函数
void greet_version_1() {
    printf("Hello from version 1!\n");
}

void greet_version_2() {
    printf("Hello from version 2!\n");
}

int main() {
    // 使用宏调用不同版本的函数
    CALL_FUNCTION(greet, 1); // 调用 greet_version_1
    CALL_FUNCTION(greet, 2); // 调用 greet_version_2

    return 0;
}

14. 结构体中使用字符数组还是字符指针

代码分析:

#include <stdio.h>
#include <string.h>

struct std {
    unsigned int id;
    char *name;
    unsigned int age;
} per;

int main(void) {
    per.id = 0001;
    strcpy(per.name, "Micheal Jackson");
    per.age = 20;

    printf("%s\n", per.name);
    return 0;
}

结论:

  • 优先使用字符数组。
  • 如果写成 per.name = "Micheal Jackson" ,只是将字符串的地址给了指针,该字符串的内容并不在结构体内,无法直接修改。

15. 内存越界问题 

分析代码:

char *p1 = "ABCABC";
char *p2 = (char *)malloc(strlen(p1));
strcpy(p2, p1);

 分析:

  1. strlen 函数:这个函数计算字符串的长度,但是不包括字符串结束符 '\0'。因此,对于字符串 "ABCABC"strlen(p1) 的结果是 6,而不是 7(如果包括 '\0')。

  2. strcpy 函数:这个函数用于复制字符串,并且会连同字符串结束符 '\0' 一起复制。


16. free 和 delete 是如何处理指针

#include <iostream>
#include <cstring>
using namespace std;

int main(void)
{
    char *p = new char[100]; // 使用new操作符分配100字节的内存空间,用于存储字符数组
    strcpy(p, "ABCABC");    // 使用strcpy函数将字符串"ABCABC"复制到p指向的内存空间
    cout << "p = " << p << endl; // 输出p指向的字符串
    delete [] p;            // 使用delete[]操作符释放p指向的内存空间

    p = NULL;              // 将p设置为NULL,避免悬垂指针问题
    return 0;
}

结论:两者只是将指针指向的内存给释放掉了,但是指针还是存在;不能再次访问的原因是此时指针是野指针,可能出现不必要的麻烦

改进操作:

  1. 释放后置空指针

    在调用 free 或 delete 后,将指针置为 NULL(C)或 nullptr(C++),避免悬空指针。
    
    free(ptr); ptr = NULL;
    
    delete ptr; ptr = nullptr;
  2. 避免重复释放:确保每个指针只释放一次。

  3. 匹配分配和释放函数

    • 使用 malloc 分配的内存用 free 释放。
    • 使用 new 分配的内存用 delete 释放。
    • 使用 new[] 分配的内存用 delete[] 释放。
  4. 检查空指针:在释放前检查指针是否为空,避免不必要的操作。

总结:

  • free 和 delete 用于释放动态分配的内存,但不会自动置空指针。
  • 释放后,指针仍然指向原来的地址,但访问它是未定义行为。
  • 最佳实践是在释放后将指针置空,并避免重复释放。

相关文章:

  • 提示学习(Prompting)
  • 算法与数据结构(二叉树中的最大路径和)
  • 深入了解 Python 中的 MRO(方法解析顺序)
  • Docker搭建基于Rust语言的云原生可观测平台OpenObserve
  • sklearn中的决策树-分类树:剪枝参数
  • PMP项目管理—整合管理篇—7.结束项目或阶段
  • 【Kubernetes】API server 限流 之 maxinflight.go
  • 跨AWS账户共享SQS队列以实现消息传递
  • SQL Server 视图的更新排查及清除缓存
  • Protobuf原理与序列化
  • 高数1.1 函数
  • 深度学习-11.用于自然语言处理的循环神经网络
  • Spring Boot集成Spring Security之HTTP请求授权
  • C++11智能指针
  • 细说 Java GC 垃圾收集器
  • springBoot统一响应类型3.1版本
  • 高举高打,阶跃星辰冲刺商业化
  • 【JavaSE-1】初识Java
  • 【Godot_4.3】预加载preload失败
  • Windows 11【1001问】删除Win11左下角小组件的6种方法
  • 国台办:民进党当局刻意刁难大陆配偶,这是不折不扣的政治迫害
  • 《蛮好的人生》:为啥人人都爱这个不完美的“大女主”
  • 复旦大学与上海杨浦共建市东医院
  • 马上评丨火车穿村而过多人被撞身亡,亡羊补牢慢不得
  • 书法需从字外看,书法家、学者吴本清辞世
  • 外交部:中方愿根据当事方意愿,为化解乌克兰危机发挥建设性作用