指针的使用规范

在C和C++中,指针是强大的工具,但如果不小心使用,可能会引发复杂的错误。因此,了解指针的规范使用原则可以帮助编写更安全和易于维护的代码。以下是一些使用指针的规范:

1. 初始化指针

指针变量在声明后,应尽量初始化,避免使用未初始化的指针导致未定义行为。常见的初始化方式包括:

  • 将指针设置为nullptr(C++11起)或NULL(C语言中):

int* ptr = nullptr; // C++
int* ptr = NULL;    // C

2. 检查指针的有效性

在解引用指针前,必须检查指针是否为空,确保它指向合法的内存地址,以防止空指针异常。

if (ptr != nullptr) {
    // 安全地使用指针
}

3. 使用指针前进行内存分配

在动态内存分配时(如mallocnew),要确保分配成功后再使用指针。

int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
    // 分配失败处理
}

4. 避免野指针

释放指针所指向的内存后,将指针设置为nullptrNULL,避免指针成为“野指针”:

free(ptr);
ptr = NULL;

5. 不要重复释放指针

重复释放会导致崩溃或不可预测的行为。确保在释放指针之前检查其是否为nullptr

6. 注意指针运算

指针运算可能导致访问非法地址。尤其在数组、字符串等场景中小心进行指针偏移操作,避免越界访问。

7. 合理使用智能指针(C++)

在C++中,使用智能指针(如std::unique_ptrstd::shared_ptr)可以减少手动管理内存的负担,防止内存泄漏:

std::unique_ptr<int> ptr = std::make_unique<int>(10);

8. 避免指针混用

当同一块内存区域有多个指针指向时,容易产生悬挂指针和未定义行为,建议尽量减少多指针对同一内存的直接操作,特别是在多线程场景下。

9. 使用const修饰符

当指针所指的值不应被修改时,使用const修饰指针或指针指向的对象,提升代码的安全性和可读性。

const int* ptr;   // 指针指向的数据不可修改
int* const ptr;   // 指针本身不可修改
const int* const ptr; // 指针和数据都不可修改

10. 注意释放动态数组内存

在使用new[]分配动态数组时,应使用delete[]来释放内存,以避免未定义行为。

int* arr = new int[10];
delete[] arr;

11. 避免返回局部变量的地址

在函数中返回局部变量的地址是一个常见错误,因为函数结束后,局部变量会被销毁。

int* func() {
    int x = 10;
    return &x; // 错误
}

按照这些规范,能够更安全地管理指针的生命周期和访问权限,从而避免许多常见的指针错误。

多资源控制问题

在多资源控制问题中,尤其是多线程或多进程环境中,合理管理和控制资源以避免竞争、死锁等问题是关键。以下是一些多资源控制的规范和建议:

1. 避免死锁(Deadlock)

死锁是多个资源竞争时常见的问题。避免死锁可以通过以下方法实现:

  • 固定资源请求顺序:所有线程按照相同的顺序申请资源。比如,始终先申请资源A再申请资源B。

  • 使用超时机制:设置资源获取的超时,当等待超时则释放已持有的资源。

  • 死锁检测:通过算法定期检测死锁情况,并根据情况释放部分资源。

2. 使用互斥锁(Mutex)保护共享资源

在多个线程访问同一共享资源时使用互斥锁,避免数据竞争问题。

  • 使用语言提供的互斥锁,如C++的std::mutex或Python的threading.Lock

  • 尽量缩小锁的范围,仅锁定访问共享资源的代码段,减少锁的持有时间,提高效率。

3. 采用读写锁(Read-Write Lock)

在读多写少的场景中,使用读写锁提高资源访问效率。读写锁允许多个读线程并发执行,但写线程必须独占。

  • C++中可以使用std::shared_mutexstd::unique_lock控制读写锁。

  • 避免多个写操作同时进行,以防止竞争和资源不一致。

4. 使用条件变量(Condition Variable)实现同步

条件变量是协调线程执行的常用工具,用于等待特定条件成立时才开始执行。

  • 在C++中使用std::condition_variable

  • 在Python中可以使用threading.Condition

  • 确保条件变量的状态在适当条件下更新,避免线程长时间等待或进入无用的忙等待状态。

5. 资源池管理

对于多线程使用频繁的资源(如数据库连接、线程、文件句柄等),可以采用资源池模式,减少创建和销毁资源的开销。

  • 在C++中可以通过对象池(如使用std::vector管理一组资源)来实现。

  • 在Python中可以使用multiprocessing.Pool管理多进程任务。

  • 注意资源池大小设置,防止资源不足或资源闲置。

6. 使用原子操作

对于简单计数、标志位等轻量资源的操作,优先使用原子操作,以减少锁的使用,提升性能。

  • C++中可以使用std::atomic操作整型变量。

  • Python中多线程环境下可以使用queuemultiprocessing.Value控制共享状态。

  • 原子操作适用于轻量、短时间占用的资源,复杂资源共享应配合互斥锁使用。

7. 避免不必要的资源共享

尽量减少资源共享的范围,尤其是跨线程共享,限制资源的使用范围,提高系统的可维护性和安全性。

  • 使用局部变量来避免不必要的共享。

  • 仅将资源共享给需要的线程或模块。

8. 选择合适的锁类型

不同场景中可以根据需求选择适合的锁类型:

  • 递归锁(Recursive Lock):允许同一线程多次申请相同的锁。适用于递归调用等情形。

  • 自旋锁(Spin Lock):适用于锁时间很短的情况,避免线程切换的开销,但不适合长时间锁定资源。

  • 互斥锁(Mutex):常用于保护较长时间的资源操作,确保线程独占访问。

9. 定期释放资源

长时间占用资源会导致资源紧张,尤其是内存、文件句柄等,应尽量释放不再使用的资源。

  • 采用RAII(Resource Acquisition Is Initialization)模式,通过对象的生命周期管理资源。

  • 定期清理资源池中的闲置资源。

10. 检查并避免活锁(Livelock)

活锁是线程或进程不断改变状态却始终无法达成目标的一种情况。避免活锁的办法包括:

  • 增加随机等待时间,减少线程间频繁相互让出资源的现象。

  • 设置一个循环检查计数器,当超出限制时强制解锁或释放资源。

通过以上多资源控制的规范和最佳实践,能够大大减少资源竞争、死锁、活锁等问题,提高程序的效率和稳定性。

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

int main (int argc, char **argv, char *envp[]) {

void ErrorProc()
{

}

    int *pA = (int *)malloc(sizeof(int));
    if(pA == NULL)
    {
        ErrorProc();
        return -1;
    }
    *pA = 999;

    char *pB1;
    float *pB2;

    if (argc % 2 == 0)
    {
        pB1 = (char *)malloc(strlen("Hello") + sizeof(char));
        if(pB1 == NULL)
        {
            free(pA);
            ErrorProc();
            return -1;
        }
        strcpy(pB1, "Hello");
    }
    else
    {
        pB2 = (float *)malloc(sizeof(float));
        if(pB2 == NULL)
        {
            free(pA);
            ErrorProc();
            return -1;
        }
        *pB2 = 3.14f;
    }

    double *pC = (double *)malloc(sizeof(double));
    if (pC == NULL)
    {
        free(pB2);
        free(pB1);
        free(pA);  
        ErrorProc();
        return -1;
    }
    *pC = 0.618;
}

在多资源管理中,堆中我们申请了A,然后分支下去B1,B2,然后又指向C。是一个菱形。这种时候我们如果发生错误,我们应该从后向前挨个释放。

#include <stdio.h>
#include <string.h>
#define SAFE_FREE(p) if(p){free(p);(p)=NULL;}

void ErrorProc()
{

}

int main (int argc, char **argv, char *envp[])
{
  //多资源使用规范
  //1.引用资源的变量在作用域开始定义, 并初始化为错误值。
  int *pA = NULL;
  int *pB1 = NULL;
  int *pB2 = NULL;
  double *pC = NULL;

  //2.申请资源后,必须检查是否资源有效,无效则处理错误
  pA = (int *)malloc(sizeof(int));
  if (pA ==NULL)
  {
    ErrorProc();
    //3.处理完错误后,跳转到统一退出位置
    goto EXIT_PROC;
  }

  //资源有效正常使用
  *pA = 999;

 EXIT_PROC:
  SAFE_FREE(pC);
  SAFE_FREE(pB2);
  SAFE_FREE(pB1);
  SAFE_FREE(pA);

  /*
    //4.释放资源前,必须要检查资源是否有效,无效则不处理
    if (pA != NULL)
    {
      free(pA);
      //5.释放资源后,必须将引用资源的变量重置为错误值
      pA = NULL;
    }
  */
  return 0;

这个结构就是标准的多资源流程,在cpp里有try catch,goto有throw

位运算

A and 1 = A

A and 0 = 0

A or 1 = 1

A xor A = 0

A xor 0 = A

A xor 1 = $\neg$A

A and $\neg$ A= 0

A or $\neg$A =

#include <stdio.h>

int my(int n )
{
  unsigned int i = 7;
  i = i >> 1; //3
  i = -1;
  i = i >> 1; //0xffffffff >> 1 = 0x7fffffff

  int j = 7;
  j = j >> 1; //3
  i = -1;
  i = i >> 1; // 0xffffffff >> 1 = oxffffffff
  // f(0x1111) -> 7(0x111) -> 3(0x11) -> 1(0x1) -> -1(-0x1) -> -1(0x1)
  // 移位前面补充0,如果是-1前面补符号位,本身不变
}

int myabs(int n)
{

  int i = n >> 31; // if n >= 0, i = 0; else i = 0xffffffff = -1
  n = n ^ i;       // if i = 0, n = n; else i = 0xffffffff, n = ~n
  return n - i;    // if i = 0, n - i = n; else i = -1, n - i = n + 1

}

int main ()
{
  int n = myabs(-5);

  return 0;
}

myabs2 函数

这个函数通过位运算实现绝对值的计算,具体步骤如下:

  • int i = n >> 31;

  • 这一步利用右移运算符获取 n 的符号位。若 n 是负数,i 会被赋值为 0xFFFFFFFF(即 -1);若 n 是正数或零,i 将为 0。

  • n = n ^ i;

  • 使用异或运算来翻转 n 的所有位。当 i 为 0 时,n 不变;当 i 为 -1 时,n 的所有位都被翻转,即 n 变成 ~n(取反)。

  • return n - i;

  • 如果 i 是 0,则返回 n;如果 i 是 -1,则返回 n + 1。这一步的目的是处理负数情况下的补偿。

main 函数

  • int fuck = 0;:定义一个整数变量。

  • scanf("%d", &fuck);:读取用户输入的整数。

  • int n = myabs2(fuck);:调用 myabs2 函数,计算绝对值并赋值给 n

  • printf("%d", n);:输出绝对值。

总结

myabs2 函数以高效且简洁的方式计算了整数的绝对值,利用了位运算和逻辑运算。myabs 函数则没有实现有效的功能,可能是一个未完成或错误的示例。总体来说,代码展示了如何使用位运算来实现简单的数学计算。

算数移动

左移和右移是位运算中的基本操作,以下是它们的规则和补位方式:

左移(<<)

  • 操作:将二进制数的所有位向左移动指定的位数。

  • 规则

  • 移动后,低位(右侧)用 0 填充。

  • 左移相当于乘以 2 的 n 次方(n 为移动的位数)。

示例

  • 0000 0001(1)左移 1 位:

0000 0001 << 1  -->  0000 0010  (2)

右移(>>)

  • 操作:将二进制数的所有位向右移动指定的位数。

  • 规则

  • 算术右移(通常用于带符号整数):最高位(符号位)保持不变,填充新位为符号位的值(1 或 0)。

    • 如果是负数,补位为 1;如果是非负数,补位为 0。

  • 逻辑右移:不考虑符号位,始终用 0 填充新位(通常用于无符号整数)。

示例

  • 算术右移

  • 对于 1111 1010(-6,补码形式)右移 1 位:

1111 1010 >> 1  -->  1111 1101  (-3)
  • 逻辑右移

  • 对于 1111 1010(-6,补码形式)逻辑右移 1 位:

1111 1010 >> 1  -->  0111 1101  (125)

总结

  • 左移:将数的所有位向左移动,用 0 填充低位。相当于乘以 2 的 n 次方。

  • 右移

  • 算术右移:保持符号位,符号位填充新位。

  • 逻辑右移:无论符号,均用 0 填充新位。

不同的编程语言可能对这两种右移的实现有所不同,所以在使用时要注意具体的语言规则。

权限控制

#define ADD 1 //0001
#define DEL 2 //0010
#define EDT 4 //0100
#define QUE 8 //1000
#include <cstdio>

int main ()
{
    int nPrivilege = 0;
    nPrivilege = nPrivilege | DEL;
    nPrivilege = nPrivilege | EDT;

    if (nPrivilege & ADD)
    {
        puts("Add Privileges");
    }

    if (nPrivilege & DEL)
    {
        puts("Delete Privileges");
    }

    if (nPrivilege & EDT)
    {
        puts("Edit Privileges");
    }

    if (nPrivilege & QUE)
    {
        puts("Queue Privileges");
    }

    return 0;
}
int nPrivilege = 0;
nPrivilege = nPrivilege | DEL;
nPrivilege = nPrivilege | EDT;

在这一部分代码中,nPrivilege 先被初始化为 0,然后通过按位或操作依次添加 DELEDT 权限。由于 DELEDT 的值分别是 20010)和 40100),执行完这两次赋值操作后,nPrivilege 的值为 6(二进制表示为 0110)。

接下来的条件判断逐一检查 nPrivilege 中是否包含 ADDDELEDTQUE 权限:

  • if (nPrivilege & ADD):不满足,因为 nPrivilege 中没有 ADD 权限。

  • if (nPrivilege & DEL):满足,nPrivilege 中包含 DEL 权限。

  • if (nPrivilege & EDT):满足,nPrivilege 中包含 EDT 权限。

  • if (nPrivilege & QUE):不满足,因为 nPrivilege 中没有 QUE 权限。

因此,程序的最终输出为:

Delete Privileges
Edit Privileges

这表示当前权限 nPrivilege 中包含 DEL(删除)和 EDT(编辑)权限。

位或与位与

位或和位与是两种常见的位运算操作,主要用于处理二进制位。它们的作用分别是设置位(位或)和检测位(位与)。我们分别来看一下它们的特点和用法。

位或(|

位或操作会将两个数的每个位进行比较,只要其中一个数的该位是 1,结果的对应位就是 1。位或主要用于设置位,即将特定的位设为 1,通常用于添加权限标志或组合多个标志。

示例

例如,将权限 ADD0001)和 DEL0010)组合到一起:

int nPrivilege = 0;
nPrivilege = nPrivilege | ADD;  // 设置 ADD 权限
nPrivilege = nPrivilege | DEL;  // 设置 DEL 权限

位或运算过程:

  0001   (ADD)
| 0010   (DEL)
  ----
  0011   (结果)

结果为 0011,表示当前权限包含 ADDDEL

位与(&

位与操作会将两个数的每个位进行比较,只有两个数的该位都是 1 时,结果的对应位才是 1。位与主要用于检测位,即检查某个位是否为 1,常用于判断是否包含某个权限。

示例

假设 nPrivilege 的值为 0011(表示包含 ADDDEL),我们可以用位与操作检查它是否包含 DEL 权限。

if (nPrivilege & DEL)
{
    puts("Delete Privileges");
}

位与运算过程:

  0011   (nPrivilege)
& 0010   (DEL)
  ----
  0010   (结果)

结果为 0010(非零),说明 nPrivilege 中包含 DEL 权限。

位或和位与的综合应用

在权限控制中,通常通过位或设置权限,通过位与检测权限。例如:

总结

  • 位或 (|):用于设置位,将特定的权限添加到 nPrivilege 中。

  • 位与 (&):用于检测位,检查 nPrivilege 是否包含特定的权限。

//描述位数关系,注释标准
//nnnnxxxxxxxxxxxxxxxxxxxxrrrr qeda
//a : add
//d : del
//e : edt
//q : que
//n : no use     未使用
//x : no define  未定义
//r : reserved   保留
#include <cstdio>

struct tagPrivilege
{
    //按位对应
    //31                          3210
    //xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    int add:1; //:1只影响一位
    int del:1;
    int edt:1;
    int que:1;
};

int main ()
{
    struct tagPrivilege pri = {0};
    pri.del = 1;
    pri.edt = 1;

    if (pri.add)
    {
        puts("add");
    }

    if (pri.del)
    {
        puts("del");
    }

    if (pri.edt)
    {
        puts("edt");
    }

    if (pri.que)
    {
        puts("que");
    }
}

这段代码定义了一个 tagPrivilege 结构体,通过位域(bit-field)来精确控制每个权限标志的占用位数。具体来说,每个权限标志 (adddeledtque) 都使用一个比特位(int :1)来表示是否拥有该权限,这样可以节省空间,并且易于按位访问。

代码详解与注释

#include <cstdio>

struct tagPrivilege
{
    // 按位对应
    // 31                          3210
    // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    int add:1; // :1 表示该字段占用 1 位,代表 "add" 权限
    int del:1; // :1 表示该字段占用 1 位,代表 "del" 权限
    int edt:1; // :1 表示该字段占用 1 位,代表 "edt" 权限
    int que:1; // :1 表示该字段占用 1 位,代表 "que" 权限
};

int main ()
{
    struct tagPrivilege pri = {0}; // 初始化所有位为 0(没有权限)
    pri.del = 1; // 设置 "del" 权限
    pri.edt = 1; // 设置 "edt" 权限

    // 检查各权限位并输出对应的权限
    if (pri.add)
    {
        puts("add");
    }

    if (pri.del)
    {
        puts("del");
    }

    if (pri.edt)
    {
        puts("edt");
    }

    if (pri.que)
    {
        puts("que");
    }
}

代码逻辑

  1. struct tagPrivilege 结构体中每个字段都使用 int :1,这表示每个字段只占用一个比特位。

  2. pri.del = 1;pri.edt = 1; 设置了 deledt 权限位,其余权限保持为 0

  3. main 函数中,通过条件判断每个权限位是否为 1,输出对应的权限。

程序的输出

由于设置了 deledt 权限,程序的输出将为:

del
edt

位定义说明

根据注释中的格式定义,可以对各字段的用途进行解释:

  • a : add - add 表示添加权限,占用 1 位。

  • d : del - del 表示删除权限,占用 1 位。

  • e : edt - edt 表示编辑权限,占用 1 位。

  • q : que - que 表示查询权限,占用 1 位。

  • n : no use - 表示未使用的位。

  • x : no define - 表示未定义的位。

  • r : reserved - 表示保留位。

每个权限使用一位,按位组合后,能够节省内存并便于权限判断。