10W+字C语言硬核总结,值得阅读收藏!

Posted C语言与CPP编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10W+字C语言硬核总结,值得阅读收藏!相关的知识,希望对你有一定的参考价值。

0.为什么使用指针

假如我们定义了 char a=’A’ ,当需要使用 ‘A’ 时,除了直接调用变量 a ,还可以定义 char *p=&a ,调用 a 的地址,即指向 a 的指针 p ,变量 a( char 类型)只占了一个字节,指针本身的大小由可寻址的字长来决定,指针 p 占用 4 个字节。

程序员必备硬核资料,点击下载

但如果要引用的是占用内存空间比较大东西,用指针也还是 4 个字节即可。

使用指针型变量在很多时候占用更小的内存空间。 变量为了表示数据,指针可以更好的传递数据,举个例子:

第一节课是 1 班语文, 2 班数学,第二节课颠倒过来, 1 班要上数学, 2 班要上语文,那么第一节课下课后需要怎样作调整呢?方案一:课间 1 班学生全都去 2 班, 2 班学生全都来 1 班,当然,走的时候要携带上书本、笔纸、零食……场面一片狼藉;方案二:两位老师课间互换教室。

显然,方案二更好一些,方案二类似使用指针传递地址,方案一将内存中的内容重新“复制”了一份,效率比较低。

  • 在数据传递时,如果数据块较大,可以使用指针传递地址而不是实际数据,即提高传输速度,又节省大量内存。

一个数据缓冲区 char buf[100] ,如果其中 buf[0,1] 为命令号, buf[2,3] 为数据类型, buf[4~7] 为该类型的数值,类型为 int ,使用如下语句进行赋值:

*(short*)&buf[0]=DataId;
*(short*)&buf[2]=DataType;
*(int*)&buf[4]=DataValue;
  • 数据转换,利用指针的灵活的类型转换,可以用来做数据类型转换,比较常用于通讯缓冲区的填充。

  • 指针的机制比较简单,其功能可以被集中重新实现成更抽象化的引用数据形式

  • 函数指针,形如: #define PMYFUN (void*)(int,int) ,可以用在大量分支处理的实例当中,如某通讯根据不同的命令号执行不同类型的命令,则可以建立一个函数指针数组,进行散转。

  • 在数据结构中,链表、树、图等大量的应用都离不开指针。

1. 指针强化

1.1 指针是一种数据类型

操作系统将硬件和软件结合起来,给程序员提供的一种对内存使用的抽象,这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作和使用真实存在的物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。

内存是一个很大的线性的字节数组,每个字节固定由 8 个二进制位组成,每个字节都有唯一的编号,如下图,这是一个 4G 的内存,他一共有 4x1024x1024x1024 = 4294967296 个字节,那么它的地址范围就是 0 ~ 4294967296 ,十六进制表示就是 0x00000000~0xffffffff ,当程序使用的数据载入内存时,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。

1.1.1 指针变量

指针是一种数据类型,占用内存空间,用来保存内存地址。

void test01(){
 
 int* p1 = 0x1234;
 int*** p2 = 0x1111;

 printf("p1 size:%d\\n",sizeof(p1));
 printf("p2 size:%d\\n",sizeof(p2));


 //指针是变量,指针本身也占内存空间,指针也可以被赋值
 int a = 10;
 p1 = &a;

 printf("p1 address:%p\\n", &p1);
 printf("p1 address:%p\\n", p1);
 printf("a address:%p\\n", &a);

}

1.1.2 野指针和空指针

1.1.2.1 空指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。

对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。

如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。 不允许向NULL和非法地址拷贝内存:

void test(){
 char *p = NULL;
 //给p指向的内存区域拷贝内容
 strcpy(p, "1111"); //err

 char *q = 0x1122;
 //给q指向的内存区域拷贝内容
 strcpy(q, "2222"); //err  
}

1.1.2.2 野指针

在使用指针时,要避免野指针的出现:

野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

什么情况下会导致野指针?

  • 指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

  • 指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

void test(){
 int* p = 0x001; //未初始化
 printf("%p\\n",p);
 *p = 100;
}

操作野指针是非常危险的操作,应该规避野指针的出现:

  • 初始化时置 NULL

指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

  • 释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

1.1.2.3 void*类型指针

void是一种特殊的指针类型,可以用来存放任意对象的地址。一个void指针存放着一个地址,这一点和其他指针类似。不同的是,我们对它到底储存的是什么对象的地址并不了解。

double a=2.3;
int b=5;
void *p=&a;
cout<<p<<endl;   //输出了a的地址

p=&b;
cout<<p<<endl;   //输出了b的地址

//cout<<*p<<endl;这一行不可以执行,void*指针只可以储存变量地址,不可以直接操作它指向的对象

由于void是空类型,只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。

1.1.2.4 void*数组和指针

  • 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝

  • 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的。指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。

  • 数组所占存储空间的内存:sizeof(数组名) 数组的大小:sizeof(数组名)/sizeof(数据类型),在32位平台下,无论指针的类型是什么,sizeof(指针名)都是 4 ,在 64 位平台下,无论指针的类型是什么,sizeof(指针名)都是 8 。

  • 数组名作为右值的时候,就是第一个元素的地址

int main(void)
{
    int arr[5] = {1,2,3,4,5};

    int *p_first = arr;
    printf("%d",*p_first);  //1
    return 0;
}
  • 指向数组元素的指针 支持 递增 递减 运算。p= p+1意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。在数组中相邻内存就是相邻下标元素。

1.1.3 间接访问操作符

通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*。

注意:对一个int类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。

int arr[5];
int *p = * (&arr);
int arr1[5][3] arr1 = int(*)[3]&arr1

1)在指针声明时,* 号表示所声明的变量为指针

2)在指针使用时,* 号表示操作指针所指向的内存空间

  • *相当通过地址(指针变量的值)找到指针指向的内存,再操作内存

  • *放在等号的左边赋值(给内存赋值,写内存)

  • *放在等号的右边取值(从内存中取值,读内存)

//解引用
void test01(){

 //定义指针
 int* p = NULL;
 //指针指向谁,就把谁的地址赋给指针
 int a = 10;
 p = &a;
 *p = 20;//*在左边当左值,必须确保内存可写
 //*号放右面,从内存中读值
 int b = *p;
 //必须确保内存可写
 char* str = "hello world!";
 *str = 'm';

 printf("a:%d\\n", a);
 printf("*p:%d\\n", *p);
 printf("b:%d\\n", b);
}

1.1.4 指针的步长

指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

思考如下问题:

int a = 0xaabbccdd;
unsigned int *p1 = &a;
unsigned char *p2 = &a;

//为什么*p1打印出来正确结果?
printf("%x\\n", *p1);
//为什么*p2没有打印出来正确结果?
printf("%x\\n", *p2);

//为什么p1指针+1加了4字节?
printf("p1  =%d\\n", p1);
printf("p1+1=%d\\n", p1 + 1);
//为什么p2指针+1加了1字节?
printf("p2  =%d\\n", p2);
printf("p2+1=%d\\n", p2 + 1);

1.1.5 函数与指针

1.1.5.1 函数的参数和指针

C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

void change(int a)
{
    a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
}
int main(void)
{
    int age = 60;
    change(age);
    printf("age = %d",age);   // age = 60
    return 0;
}

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

传递变量的指针可以轻松解决上述问题。

void change(int* pa)
{
    (*pa)++;   //因为传递的是age的地址,因此pa指向内存数据age。当在函数中对指针pa解地址时,
               //会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
    int age = 160;
    change(&age);
    printf("age = %d",age);   // age = 61
    return 0;
}

比如指针的一个常见的使用例子:

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

void swap(int *,int *);
int main()
{
    int a=5,b=10;
    printf("a=%d,b=%d\\n",a,b);
    swap(&a,&b);
    printf("a=%d,b=%d\\n",a,b);
    return 0;
}
void swap(int *pa,int *pb)
{
    int t=*pa;*pa=*pb;*pb=t;
}

在以上的例子中,swap函数的两个形参pa和pb可以接收两个整型变量的地址,并通过间接访问的方式修改了它指向变量的值。在main函数中调用swap时,提供的实参分别为&a,&b,这样就实现了pa=&a,pb=&b的赋值过程,这样在swap函数中就通过pa修改了 a 的值,通过pb修改了 b 的值。因此,如果需要在被调函数中修改主调函数中变量的值,就需要经过以下几个步骤:

  • 定义函数的形参必须为指针类型,以接收主调函数中传来的变量的地址;

  • 调用函数时实参为变量的地址;

  • 在被调函数中使用*间接访问形参指向的内存空间,实现修改主调函数中变量值的功能。

指针作为函数的形参的另一个典型应用是当函数有多个返回值的情形。比如,需要在一个函数中统计一个数组的最大值、最小值和平均值。当然你可以编写三个函数分别完成统计三个值的功能。但比较啰嗦,如:

int GetMax(int a[],int n)
{
    int max=a[0],i;
    for(i=1;i<n;i++)
    {
        if(max<a[i]) max=a[i];
    }
    return max;
}
int GetMin(int a[],int n)
{
    int min=a[0],i;
    for(i=1;i<n;i++)
    {
        if(min>a[i]) min=a[i];
    }
    return min;
}
double GetAvg(int a[],int n)
{
    double avg=0;
    int i;
    for(i=0;i<n;i++)
    {
        avg+=a[i];
    }
    return avg/n;
}

其实我们完全可以在一个函数中完成这个功能,由于函数只能有一个返回值,可以返回平均值,最大值和最小值可以通过指针类型的形参来进行实现:

double Stat(int a[],int n,int *pmax,int *pmin)
{
    double avg=a[0];
    int i;
    *pmax=*pmin=a[0];
    for(i=1;i<n;i++)
    {
        avg+=a[i];
        if(*pmax<a[i]) *pmax=a[i];
        if(*pmin>a[i]) *pmin=a[i];
    }
    return avg/n;
}

1.1.5.2 函数的指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址。我们可以把函数的这个首地址赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

用指针来实现对函数的调用:

#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b)
{
    return a>b ? a : b;
}
int main()
{
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\\n", maxval);
    return 0;
}

1.1.5.3 结构体和指针

结构体指针有特殊的语法: -> 符号

如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员

typedef struct
{
    char name[31];
    int age;
    float score;
}Student;

int main(void)
{
    Student stu = {"Bob" , 19, 98.0};
    Student*ps = &stu;

    ps->age = 20;
    ps->score = 99.0;
    printf("name:%s age:%d
",ps->name,ps->age);
    return 0;
}

1.2 指针的意义_间接赋值

1.2.1 间接赋值的三大条件

通过指针间接赋值成立的三大条件:

  • 2个变量(一个普通变量一个指针变量、或者一个实参一个形参)

  • 建立关系

  • 通过 * 操作指针指向的内存

void test(){
 int a = 100; //两个变量
 int *p = NULL;
 //建立关系
 //指针指向谁,就把谁的地址赋值给指针
 p = &a;
 //通过*操作内存
 *p = 22;
}

1.2.2 如何定义合适的指针变量

void test(){
 int b;  
 int *q = &b; //0级指针
 int **t = &q;
 int ***m = &t;
}

1.2.3 间接赋值:从0级指针到1级指针

int func1(){ return 10; }

void func2(int a){
 a = 100;
}
//指针的意义_间接赋值
void test02(){
 int a = 0;
 a = func1();
 printf("a = %d\\n", a);

 //为什么没有修改?
 func2(a);
 printf("a = %d\\n", a);
}

//指针的间接赋值
void func3(int* a){
 *a = 100;
}

void test03(){
 int a = 0;
 a = func1();
 printf("a = %d\\n", a);

 //修改
 func3(&a);
 printf("a = %d\\n", a);
}

1.2.4 间接赋值:从1级指针到2级指针

void AllocateSpace(char** p){
 *p = (char*)malloc(100);
 strcpy(*p, "hello world!");
}

void FreeSpace(char** p){

 if (p == NULL){
  return;
 }
 if (*p != NULL){
  free(*p);
  *p = NULL;
 }

}

void test(){
 
 char* p = NULL;

 AllocateSpace(&p);
 printf("%s\\n",p);
 FreeSpace(&p);

 if (p == NULL){
  printf("p内存释放!\\n");
 }
}

1.2.4 间接赋值的推论

  • 用1级指针形参,去间接修改了0级指针(实参)的值。

  • 用2级指针形参,去间接修改了1级指针(实参)的值。

  • 用3级指针形参,去间接修改了2级指针(实参)的值。

  • 用n级指针形参,去间接修改了n-1级指针(实参)的值。

1.3 指针做函数参数

指针做函数参数,具备输入和输出特性:

  • 输入:主调函数分配内存

  • 输出:被调用函数分配内存

1.3.1 输入特性

void fun(char *p /* in */)
{
 //给p指向的内存区域拷贝内容
 strcpy(p, "abcddsgsd");
}

void test(void)
{
 //输入,主调函数分配内存
 char buf[100] = { 0 };
 fun(buf);
 printf("buf  = %s\\n", buf);
}

1.3.2 输出特性

void fun(char **p /* out */, int *len)
{
 char *tmp = (char *)malloc(100);
 if (tmp == NULL)
 {
  return;
 }
 strcpy(tmp, "adlsgjldsk");

 //间接赋值
 *p = tmp;
 *len = strlen(tmp);
}

void test(void)
{
 //输出,被调用函数分配内存,地址传递
 char *p = NULL;
 int len = 0;
 fun(&p, &len);
 if (p != NULL)
 {
  printf("p = %s, len = %d\\n", p, len);
 }
}

1.4 字符串指针强化

1.4.1 字符串指针做函数参数

1.4.1.1 字符串基本操作

//字符串基本操作
//字符串是以0或者'\\0'结尾的字符数组,(数字0和字符'\\0'等价)
void test01(){

 //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
 char str1[] = { 'h', 'e', 'l', 'l', 'o' };
 printf("%s\\n",str1);

 //字符数组部分初始化,剩余填0
 char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
 printf("%s\\n", str2);

 //如果以字符串初始化,那么编译器默认会在字符串尾部添加'\\0'
 char str3[] = "hello";
 printf("%s\\n",str3);
 printf("sizeof str:%d\\n",sizeof(str3));
 printf("strlen str:%d\\n",strlen(str3));

 //sizeof计算数组大小,数组包含'\\0'字符
 //strlen计算字符串的长度,到'\\0'结束

 //那么如果我这么写,结果是多少呢?
 char str4[100] = "hello";
 printf("sizeof str:%d\\n", sizeof(str4));
 printf("strlen str:%d\\n", strlen(str4));

 //请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
 char str5[] = "hello\\0world"; 
 printf("%s\\n",str5);
 printf("sizeof str5:%d\\n",sizeof(str5));
 printf("strlen str5:%d\\n",strlen(str5));

 //再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
 char str6[] = "hello\\012world";
 printf("%s\\n", str6);
 printf("sizeof str6:%d\\n", sizeof(str6));
 printf("strlen str6:%d\\n", strlen(str6));
}

八进制和十六进制转义字符:

在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是'\\ddd',d是0-7的数字。十六进制字符的一般形式是'\\xhh',h是0-9A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。

比如 :

  • '\\063'表示的是字符'3',因为'3'的ASCII码是30(十六进制),48(十进制),63(八进制)。

  • '\\x41'表示的是字符'A',因为'A'的ASCII码是41(十六进制),65(十进制),101(八进制)。

1.4.1.2 字符串拷贝功能实现

//拷贝方法1
void copy_string01(char* dest, char* source ){

 for (int i = 0; source[i] != '\\0';i++){
  dest[i] = source[i];
 }

}

//拷贝方法2
void copy_string02(char* dest, char* source){
 while (*source != '\\0' /* *source != 0 */){
  *dest = *source;
  source++;
  dest++;
 }
}

//拷贝方法3
void copy_string03(char* dest, char* source){
 //判断*dest是否为0,0则退出循环
 while (*dest++ = *source++){}
}

程序员必备硬核资料,点击下载

1.4.1.3 字符串反转模型

void reverse_string(char* str){

 if (str == NULL){
  return;
 }

 int begin = 0;
 int end = strlen(str) - 1;
 
 while (begin < end){
  
  //交换两个字符元素
  char temp = str[begin];
  str[begin] = str[end];
  str[end] = temp;

  begin++;
  end--;
 }

}

void test(){
 char str[] = "abcdefghijklmn";
 printf("str:%s\\n", str);
 reverse_string(str);
 printf("str:%s\\n", str);
}

1.4.2 字符串的格式化

1.4.2.1 sprintf

#include <stdio.h>
int sprintf(char *str, const char *format, ...);

功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到 出现字符串结束符 '\\0' 为止。

参数

  • str:字符串首地址

  • format:字符串格式,用法和printf()一样

返回值

  • 成功:实际格式化的字符个数

  • 失败: - 1

void test(){
 
 //1. 格式化字符串
 char buf[1024] = { 0 };
 sprintf(buf, "你好,%s,欢迎加入我们!", "John");
 printf("buf:%s\\n",buf);

 memset(buf, 0, 1024);
 sprintf(buf, "我今年%d岁了!", 20);
 printf("buf:%s\\n", buf);

 //2. 拼接字符串
 memset(buf, 0, 1024);
 char str1[] = "hello";
 char str2[] = "world";
 int len = sprintf(buf,"%s %s",str1,str2);
 printf("buf:%s len:%d\\n", buf,len);

 //3. 数字转字符串
 memset(buf, 0, 1024);
 int num = 100;
 sprintf(buf, "%d", num);
 printf("buf:%s\\n", buf);
 //设置宽度 右对齐
 memset(buf, 0, 1024);
 sprintf(buf, "%8d", num);
 printf("buf:%s\\n", buf);
 //设置宽度 左对齐
 memset(buf, 0, 1024);
 sprintf(buf, "%-8d", num);
 printf("buf:%s\\n", buf);
 //转成16进制字符串 小写
 memset(buf, 0, 1024);
 sprintf(buf, "0x%x", num);
 printf("buf:%s\\n", buf);

 //转成8进制字符串
 memset(buf, 0, 1024);
 sprintf(buf, "0%o", num);
 printf("buf:%s\\n", buf);
}

1.4.2.2 sscanf

#include <stdio.h>
int sscanf(const char *str, const char *format, ...);

功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。

参数

  • str:指定的字符串首地址

  • format:字符串格式,用法和scanf()一样

返回值

  • 成功:成功则返回参数数目,失败则返回-1

  • 失败: - 1

//1. 跳过数据
void test01(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //匹配第一个字符是否是数字,如果是,则跳过
 //如果不是则停止匹配
 sscanf("123456aaaa", "%*d%s", buf); 
 printf("buf:%s\\n",buf);
}

//2. 读取指定宽度数据
void test02(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 sscanf("123456aaaa", "%7s", buf);
 printf("buf:%s\\n", buf);
}

//3. 匹配a-z中任意字符
void test03(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符,判断字符是否是a-z中的字符,如果是匹配
 //如果不是停止匹配
 sscanf("abcdefg123456", "%[a-z]", buf);
 printf("buf:%s\\n", buf);
}

//4. 匹配aBc中的任何一个
void test04(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("abcdefg123456", "%[aBc]", buf);
 printf("buf:%s\\n", buf);
}

//5. 匹配非a的任意字符
void test05(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("bcdefag123456", "%[^a]", buf);
 printf("buf:%s\\n", buf);
}

//6. 匹配非a-z中的任意字符
void test06(){
 char buf[1024] = { 0 };
 //跳过前面的数字
 //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
 sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
 printf("buf:%s\\n", buf);
}

1.5 一级指针易错点

1.5.1 越界

void test(){
 char buf[3] = "abc";
 printf("buf:%s\\n",buf);
}

1.5.2 指针叠加会不断改变指针指向

void test(){
 char *p = (char *)malloc(50);
 char buf[] = "abcdef";
 int n = strlen(buf);
 int i = 0;

 for (i = 0; i < n; i++)
 {
  *p = buf[i];
  p++; //修改原指针指向
 }

 free(p);
}

1.5.3 返回局部变量地址

char *get_str()
{
 char str[] = "abcdedsgads"; //栈区,
 printf("[get_str]str = %s\\n", str);
 return str;
}

1.5.4 同一块内存释放多次(不可以释放野指针)

void test(){ 
 char *p = NULL;

 p = (char *)malloc(50);
 strcpy(p, "abcdef");

 if (p != NULL)
 {
  //free()函数的功能只是告诉系统 p 指向的内存可以回收了
  // 就是说,p 指向的内存使用权交还给系统
  //但是,p的值还是原来的值(野指针),p还是指向原来的内存
  free(p); 
 }

 if (p != NULL)
 {
  free(p);
 }
}

1.6 const使用

//const修饰变量
void test01(){
 //1. const基本概念
 const int i = 0;
 //i = 100; //错误,只读变量初始化之后不能修改

 //2. 定义const变量最好初始化
 const int j;
 //j = 100; //错误,不能再次赋值

 //3. c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改
 const int k = 10;
 //k = 100; //错误,不可直接修改,我们可通过指针间接修改
 printf("k:%d\\n", k);
 int* p = &k;
 *p = 100;
 printf("k:%d\\n", k);
}

//const 修饰指针
void test02(){

 int a = 10;
 int b = 20;
 //const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
 const int* p_a = &a;
 //*p_a = 100; //不可修改指针指向的内存空间
 p_a = &b; //可修改指针的指向

 //const放在*号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间
 int* const p_b = &a;
 //p_b = &b; //不可修改指针的指向
 *p_b = 100; //可修改指针指向的内存空间

 //指针的指向和指针指向的内存空间都不能修改
 const int* const p_c = &a;
}
//const指针用法
struct Person{
 char name[64];
 int id;
 int age;
 int score;
};

//每次都对对象进行拷贝,效率低,应该用指针
void printPersonByValue(struct Person person){
 printf("Name:%s\\n", person.name);
 printf("Name:%d\\n", person.id);
 printf("Name:%d\\n", person.age);
 printf("Name:%d\\n", person.score);
}

//但是用指针会有副作用,可能会不小心修改原数据
void printPersonByPointer(const struct Person *person){
 printf("Name:%s\\n", person->name);
 printf("Name:%d\\n", person->id);
 printf("Name:%d\\n", person->age);
 printf("Name:%d\\n", person->score);
}
void test03(){
 struct Person p = { "Obama", 1101, 23, 87 };
 //printPersonByValue(p);
 printPersonByPointer(&p);
}

2. 指针的指针(二级指针)

2.1 二级指针基本概念

这里让我们花点时间来看一个例子,揭开这个即将开始的序幕。考虑下面这些声明:

int a = 12;
int *b = &a;

它们如下图进行内存分配:

假定我们又有了第3个变量,名叫c,并用下面这条语句对它进行初始化:

c = &b;

它在内存中的大概模样大致如下:

c的类型是什么?显然它是一个指针,但它所指向的是什么?

变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。

它合法吗?

是的!指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的。

那么这个变量的声明是怎样的声明的呢?

int **c = &b;

那么这个**c如何理解呢?操作符具有从右想做的结合性,所以这个表达式相当于(*c),我们从里向外逐层求职。*c访问c所指向的位置,我们知道这是变量b.第二个间接访问操作符访问这个位置所指向的地址,也就是变量a.指针的指针并不难懂,只需要留心所有的箭头,如果表达式中出现了间接访问操作符,你就要随箭头访问它所指向的位置。

2.2 二级指针做形参输出特性

二级指针做参数的输出特性是指由被调函数分配内存。

//被调函数,由参数n确定分配多少个元素内存
void allocate_space(int **arr,int n){
 //堆上分配n个int类型元素内存
 int *temp = (int *)malloc(sizeof(int)* n);
 if (NULL == temp){
  return;
 }
 //给内存初始化值
 int *pTemp = temp;
 for (int i = 0; i < n;i ++){
  //temp[i] = i + 100;
  *pTemp = i + 100;
  pTemp++;
 }
 //指针间接赋值
 *arr = temp;
}
//打印数组
void print_array(int *arr,int n){
 for (int i = 0; i < n;i ++){
  printf("%d ",arr[i]);
 }
 printf("\\n");
}
//二级指针输出特性(由被调函数分配内存)
void test(){
 int *arr = NULL;
 int n = 10;
 //给arr指针间接赋值
 allocate_space(&arr,n);
 //输出arr指向数组的内存
 print_array(arr, n);
 //释放arr所指向内存空间的值
 if (arr != NULL){
  free(arr);
  arr = NULL;
 }
}

2.3 二级指针做形参输入特性

二级指针做形参输入特性是指由主调函数分配内存。

//打印数组
void print_array(int **arr,int n){
 for (int i = 0; i < n;i ++){
  printf("%d ",*(arr[i]));
 }
 printf("\\n");
}
//二级指针输入特性(由主调函数分配内存)
void test(){
 
 int a1 = 10;
 int a2 = 20;
 int a3 = 30;
 int a4 = 40;
 int a5 = 50;

 int n = 5;

 int** arr = (int **)malloc(sizeof(int *) * n);
 arr[0] = &a1;
 arr[1] = &a2;
 arr[2] = &a3;
 arr[3] = &a4;
 arr[4] = &a5;

 print_array(arr,n);

 free(arr);
 arr = NULL;
}

2.4 强化训练_画出内存模型图

void mian()
{
 //栈区指针数组
 char *p1[] = { "aaaaa", "bbbbb", "ccccc" };

 //堆区指针数组
 char **p3 = (char **)malloc(3 * sizeof(char *)); //char *array[3];

 int i = 0;
 for (i = 0; i < 3; i++)
 {
  p3[i] = (char *)malloc(10 * sizeof(char)); //char buf[10]
  sprintf(p3[i], "%d%d%d", i, i, i);
 }
}

2.4 多级指针

将堆区数组指针案例改为三级指针案例:

//分配内存
void allocate_memory(char*** p, int n){

 if (n < 0){
  return;
 }

 char** temp = (char**)malloc(sizeof(char*)* n);
 if (temp == NULL){
  return;
 }

 //分别给每一个指针malloc分配内存
 for (int i = 0; i < n; i++){
  temp[i] = malloc(sizeof(char)* 30);
  sprintf(temp[i], "%2d_hello world!", i + 1);
 }

 *p = temp;
}

//打印数组
void array_print(char** arr, int len){
 for (int i = 0; i < len; i++){
  printf("%s\\n", arr[i]);
 }
 printf("----------------------\\n");
}

//释放内存
void free_memory(char*** buf, int len){
 if (buf == NULL){
  return;
 }

 char** temp = *buf;

 for (int i = 0; i < len; i++){
  free(temp[i]);
  temp[i] = NULL;
 }

 free(temp);
}

void test(){

 int n = 10;
 char** p = NULL;
 allocate_memory(&p, n);
 //打印数组
 array_print(p, n);
 //释放内存
 free_memory(&p, n);
}

2.5 深拷贝和浅拷贝

如果2个程序单元(例如2个函数)是通过拷贝 他们所共享的数据的 指针来工作的,这就是浅拷贝,因为真正要访问的数据并没有被拷贝。如果被访问的数据被拷贝了,在每个单元中都有自己的一份,对目标数据的操作相互 不受影响,则叫做深拷贝。

#include <iostream>
using namespace std;

class CopyDemo
{
public:
  CopyDemo(int pa,char *cstr)  //构造函数,两个参数
  {
     this->a = pa;
     this->str = new char[1024]; //指针数组,动态的用new在堆上分配存储空间
     strcpy(this->str,cstr);    //拷贝过来
  }

//没写,C++会自动帮忙写一个复制构造函数,浅拷贝只复制指针,如下注释部分
  //CopyDemo(CopyDemo& obj)  
  //{
  //   this->a = obj.a;
  //  this->str = obj.str; //这里是浅复制会出问题,要深复制
  //}

  CopyDemo(CopyDemo& obj)  //一般数据成员有指针要自己写复制构造函数,如下
  {
     this->a = obj.a;
    // this->str = obj.str; //这里是浅复制会出问题,要深复制
     this->str = new char[1024];//应该这样写
     if(str != 0)
        strcpy(this->str,obj.str); //如果成功,把内容复制过来
  }

  ~CopyDemo()  //析构函数
  {
     delete str;
  }

public:
     int a;  //定义一个整型的数据成员
     char *str; //字符串指针
};

int main()
{
  CopyDemo A(100,"hello!!!");

  CopyDemo B = A;  //复制构造函数,把A的10和hello!!!复制给B
  cout <<"A:"<< A.a << "," <<A.str << endl;
  //输出A:100,hello!!!
  cout <<"B:"<< B.a << "," <<B.str << endl;
  //输出B:100,hello!!!

  //修改后,发现A,B都被改变,原因就是浅复制,A,B指针指向同一地方,修改后都改变
  B.a = 80;
  B.str[0] = 'k';

  cout <<"A:"<< A.a << "," <<A.str << endl;
  //输出A:100,kello!!!
  cout <<"B:"<< B.a << "," <<B.str << endl;
  //输出B:80,kello!!!

  return 0;
}

根据上面实例可以看到,浅复制仅复制对象本身(其中包括是指针的成员),这样不同被复制对象的成员中的对应非空指针会指向同一对象,被成员指针引用的对象成为共享的,无法直接通过指针成员安全地删除(因为若直接删除,另外对象中的指针就会无效,形成所谓的野指针,而访问无效指针是危险的;

除非这些指针有引用计数或者其它手段确保被指对象的所有权);而深复制在浅复制的基础上,连同指针指向的对象也一起复制,代价比较高,但是相对容易管理。

程序员必备硬核资料,点击下载

参考资料

  1. C Primer Plus(第五版)中文版

  2. https://www.cnblogs.com/lulipro/p/7460206.html

欢迎一键三连!!!

以上是关于10W+字C语言硬核总结,值得阅读收藏!的主要内容,如果未能解决你的问题,请参考以下文章

10W+字C语言硬核总结,值得阅读收藏!

要看就看最好,16万字全网最硬核redis总结,谁赞成,谁反对?(被粉丝疯狂催更,已有人反馈看完专栏拿到大厂offer!!!)

要看就看最好,16万字全网最硬核redis总结,谁赞成,谁反对?(被粉丝疯狂催更,已有人反馈看完专栏拿到大厂offer!!!)

万字长文超硬核详细学习系列——深入浅出Linux基础篇的知识点,值得你收藏学习必备

万字长文超硬核详细学习系列——深入浅出Linux高级篇的知识点,值得你收藏学习必备

超硬核!16000 字 Redis 面试知识点总结,这还不赶紧收藏?