天天看點

C語言深度剖析之查漏補缺

C語言深度剖析之查漏補缺,進行了總結。

今日總結:

一、const修飾指針的三種效果,C語言const修飾指針詳解

二、char *str 和 char str[] 的差別

三、malloc在堆上動态配置設定記憶體

四、結構體中字元數組指派字元串問題

一、const修飾指針的三種效果,C語言const修飾指針詳解

問題描述:const修飾的指針變量?

前面講過,當一個變量用 const 修飾後就不允許改變它的值了。那麼如果在定義指針變量的時候用 const 修飾會怎樣?同樣必須要在定義的時候進行初始化。比如:

int  a;
    int  *p = &a;      

當用 const 進行修飾時,根據 const 位置的不同有三種效果。原則是:修飾誰,誰的内容就不可變,其他的都可變。這三種情況在面試的時候幾乎是必考的,在實際程式設計中也是經常使用的,是以初學者一定要掌握。

1) const int*p=&a;      

同樣 const 和 int 可以互換位置,二者是等價的。我們以放在最前面時進行描述。

當把 const 放最前面的時候,它修飾的就是 *p,那麼 *p 就不可變。*p 表示的是指針變量 p 所指向的記憶體單元裡面的内容,此時這個内容不可變。其他的都可變,如 p 中存放的是指向的記憶體單元的位址,這個位址可變,即 p 的指向可變。但指向誰,誰的内容就不可變。

這種用法常見于定義函數的形參。前面學習 printf 和 scanf,以及後面将要學習的很多函數,它們的原型中很多參數都是用 const 修飾的,這樣做的好處是安全!我們通過參數傳遞資料時,就把資料暴露了。而大多數情況下隻是想使用傳過來的資料,并不想改變它的值,但往往由于程式設計人員個人水準的原因會不小心改變它的值。這時我們在形參中用 const 把傳過來的資料定義成隻讀的,這樣就更安全了。這也是 const 最有用之處。

是以如果你不想改變某個參數傳過來的值,那麼定義函數時就最好用 const 修飾這個參數,否則就不要用 const 修飾了。

2) int*const p=&a;

此時 const 修飾的是 p,是以 p 中存放的記憶體單元的位址不可變,而記憶體單元中的内容可變。即 p 的指向不可變,p 所指向的記憶體單元的内容可變。

3) const int*const p=&a;

此時 *p 和 p 都被修飾了,那麼 p 中存放的記憶體單元的位址和記憶體單元中的内容都不可變。

綜上所述,使用 const 可以保護用指針通路記憶體時由指針導緻的被通路記憶體空間中資料的誤更改。因為指針是直接通路記憶體的,沒有拷貝,而有些時候使用指針通路記憶體時并不是要改變裡面的值,而隻是要使用裡面的值,是以一旦不小心誤操作把裡面的資料改了就糟糕了。

但是這裡需要注意的是,上面第 1 種情況中,雖然在 *p 前加上 const 可以禁止指針變量 p 修改變量 a 中的值,但是它隻能“禁止指針變量 p 修改”。也就是說,它隻能保證在使用指針變量 p 時,p 不能修改 a 中的值。但是我并沒有說 const 可以保護 a 禁止一切的修改,其他指向 a 的沒有用 const 修飾的指針變量照樣可以修改 a 的值,而且變量 a 自己也可以修改自己的值。下面寫一個程式看一下:

# include <stdio.h>
    int main(void)
    {   
        int a = 10;
        const int *p = &a;
        int * q = &a;
        *q = 20;
        printf("a = %d\n", a);
        a = 30;
        printf("a = %d\n", a);
        //*p = 30;  //這麼寫就是錯的
        return 0;
    }      

輸出結果是:

a = 20

a = 30

可見,隻有用 const 修飾過的指針變量 p 不能修改 a 中的内容,而沒有用 const 修飾過的指針變量 q 照樣可以修改 a 中的内容,而且 a 自己也可以重新給自己指派。

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

int main(void)
{
    //const修飾一個變量為隻讀
    const int a = 10;
    //a = 100; //err

    //指針變量, 指針指向的記憶體, 2個不同概念
    char buf[] = "aklgjdlsgjlkds";

    //從左往右看,跳過類型,看修飾哪個字元
    //如果是*, 說明指針指向的記憶體不能改變
    //如果是指針變量,說明指針的指向不能改變,指針的值不能修改
    const char *p = buf;
    // 等價于上面 char const *p1 = buf;
    //p[1] = '2'; //err
    p = "agdlsjaglkdsajgl"; //ok

    char * const p2 = buf;
    p2[1] = '3';
    //p2 = "salkjgldsjaglk"; //err

    //p3為隻讀,指向不能變,指向的記憶體也不能變
    const char * const p3 = buf;

    return 0;
}      

擴充:const修飾結構體指針形參變量

//結構體類型的定義
struct stu
{
    char name[50];
    int age;
};

void fun1(struct stu * const p)
{
    //p = NULL; //err
    p->age = 10; //ok
}

//void fun2(struct stu const*  p)
void fun2(const struct stu *  p)
{
    p = NULL; //ok
    //p->age = 10; //err
}

void fun3(const struct stu * const p)
{
    //p = NULL; //err
    //p->age = 10; //err
}      

二、char *str 和 char str[] 的差別

問題描述:為什麼主函數中不能用char *str = "abcdefg";?

相同點:

都是定義一個字元串

不同點:

1)含義上的差別

char str[] 是定義一個字元串數組,數組對應的是一塊記憶體區域,而char *str 是定義一個指向字元串的指針,即指向一塊記憶體區域。

數組的記憶體大小和其位址在作用域裡是固定不變的,隻有它存儲的内容可以改變;而指針卻不同,它指向的記憶體區域的大小随時可以改變,而且當指針指向常量字元串時,它指向的内容是不可以被修改的,否則在運作時會報錯。

看一個例子:

#include<stdio.h>
#include<string.h>
int main()
{
    char str1[] = "Hello";
    char str2[] = "World";
    strcpy(str1,str2);
    printf("%s\n",str1);
    return 0;
}      

上面這段代碼是可以運作的,因為數組的内容是可以被修改的。

再看下面這段代碼:

#include<stdio.h>
#include<string.h>
int main()
{
    char *str1 = "Hello";
    char *str2 = "World";
    strcpy(str1,str2);
    printf("%s\n",str1);
    return 0;
}      

這段代碼在編譯時不會報錯,但是在運作時結果會出錯,原因在于這段代碼企圖修改 str1 的内容,由于 str1 和 str2 是指向常量字元串的指針,其内容是不可被修改的,是以在運作時會出錯。

》擴充:

下面介紹一下char str[]="hello"與char *str="hello"的差別

char str[]="hello";

第一個表達式表示的是在動态變量區中開辟一個能連續放6個字元的數組,數組名稱是str.而指派運算符右邊是一個字元串常量,這個字元串常量是存放在常量區的,這個表達式的意思就是将“hello”這個字元串常量拷貝到剛才開辟的數組中。C語言規定,表達式如果是一個數組名,則代表的意思是該數組的起始位址,如果這個數組在一個函數中定義,如果以數組名傳回時,因為數組在函數中定義,是個局部變量,函數傳回之後,這個數組所占用的空間就被釋放掉了,數組也被破壞掉了,是以傳回的數組名也就沒有意義,不能被其主調函數使用了。

如果我們這樣寫:

char *foo()
{
char str[]="hello";
return str;
}      

在編譯的時候就會出現警告:函數傳回局部變量的位址

再來看下char *str="hello"

這個表達式的意思是在動态變量區中開辟一個存放指針的存儲單元,指針變量名是str,"hello"同樣也是一個字元串常量,存儲在常量區,在程式的運作過程中一直存在。把字元串“hello”的位址值拷貝到剛才的存儲單元中,即指針變量str的初值是字元串“hello”的位址。這時如果char *str="hello"定義在一個函數中并且以return str傳回,因為str是一個變量名,傳回的僅僅是str的值,是以在其他函數中可以使用該值,照樣能夠通路到“hello”這個字元串。

#include<stdio.h>
#include<string.h>
char* foo()
{
    char *str="hello";
    return str;
}
int main()
{
    char *q;
    q=foo();
    printf("q=%s\n",q);
}      

2)計算記憶體的差別

用 sizeof 可以直接計算出數組的位元組數;而指針的大小隻與編譯器所處的平台有關,在32位平台上(例如X86)指針占4個位元組,而在64位平台上(例如X64)指針占8個位元組。

另外在進行參數傳遞時,數組會退化成為同類型的指針。

練習:字元串反轉模型

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

int inverse(char *p)
{
    if (p == NULL)
    {
        return -1;
    }
    char *str = p;
    int begin = 0;
    int end = strlen(str) - 1;
    char tmp;

    while (begin < end)
    {
        //交換元素
        tmp = str[begin];
        str[begin] = str[end];
        str[end] = tmp;

        begin++;  //往右移動位置
        end--;        //往左移動位置
    }

    return 0;
}

int main(void)
{
    //char *str = "abcdefg"; //檔案常量區,内容不允許修改
    char str[] = "abcdef";

    int ret = inverse(str);
    if (ret != 0)
    {
        return ret;
    }

    printf("str ========== %s\n", str);
    return 0;
}      

三、malloc在堆上動态配置設定記憶體

問題描述:malloc在子函數中配置設定的空間為什麼不能通過值傳遞傳回給主函數?

1)通過傳回值配置設定記憶體

int *getspace()
{   
    //malloc函數的傳回值是配置設定區域的起始位址
    //函數原型為void *malloc(unsigned int size);
    //其作用是在記憶體的動态存儲區中配置設定一個長度為size的連續空間
    int *p = (int *)malloc(sizeof(int) * 5);//在堆上動态配置設定記憶體
    if (NULL == p)
    {
        return NULL;
    }
    //往配置設定的記憶體裡面寫值
    for (int i = 0; i < 5; ++i)//++i 比i++ 的效率高
    {
    //隻要是連續的空間都能使用下标的方式通路
        p[i] = 100 + i;
    }
    return p;

}
void  test()
{
    int *ret = getspace();
    for (int i = 0; i < 5; ++i)
    {
        cout << ret[i] << endl;
    }

    free(ret);//釋放堆上動态配置設定的記憶體
    ret = NULL;//避免出現野指針

}      

2)通過參數傳遞指針配置設定記憶體

(1)錯誤寫法

這種寫法程式運作會死掉,原因是allocateSpace(ret)函數執行後,參數p指向malloc函數配置設定的記憶體0x88(舉例),allocateSpace執行完後p被釋放0x88記憶體洩漏,ret指向為空

void allocateSpace(char *p)
{
    p= (char *)malloc(100);
    memset(p, 0, 100);
    strcpy(p,"hello world");
}
void  test1()
{
    char *ret = NULL;
    allocateSpace(ret);
    cout << "ret=" << ret;
}      

記憶體空間模型分析:

C語言深度剖析之查漏補缺

(2)正确的寫法

将&ret傳入allocateSpace

void allocateSpace(char **p)
{
    char *temp = (char *)malloc(100);
    //memset() 函數可新申請的記憶體進行初始化工作
    //将指針變量 p 所指向的前 100 位元組的記憶體單元用一個“整數” 0 替換
    memset(temp, 0, 100);
    strcpy(temp,"hello world");
    *p = temp;//*p=ret,是以實際ret指向malloc配置設定的記憶體空間
}
void  test12()
{
    char *ret = NULL;
    allocateSpace(&ret);
    cout << "ret=" << ret;//結果ret=hello world
}      

四、結構體中字元數組指派字元串問題

問題描述:結構體中的字元數組為什麼不能通過s1.c="abc";指派?

今天在看結構體變量時發現一個問題:

問題代碼如下

int main() {
    struct student{
        char c[20];
    }s1;
    s1.c="china";
    printf("%s",s1.c);
    return 0;
}      

輸出結果為空,不知道是為什麼?

經過網上的答案盡是如此:

C語言隻有在定義字元數組的時候才能用“=”來初始化變量,其它情況下是不能直接用“=”來為字元數組指派的,要為字元數組指派可以用string.h頭檔案中的strcpy函數來完成。

例如:

char a[10] = "123"; /*正确,在定義的時候初始化*/
char a[10];
a = "123"; /*錯誤,不能用“=”直接為字元數組指派*/
strcpy(a, "123"); /*正确,使用strcpy函數複制字元串*/      

是以要對game[0][0].cpart指派應該用strcpy(game[0][0].cpart, "123");才對。注意要使用strcpy函數要用#include <string.h>包含string.h頭檔案。

給C語言結構體中的char數組指派有兩種方式:

(1)在聲明結構體變量時指派:

//#include "stdafx.h"//If the vc++6.0, with this line.
#include "stdio.h"
struct stu{
    int x;
    char name[10];
};

int main(void){
    struct stu s={8,"123"};//這樣初始化
    printf("%d %s\n",s.x,s.name);
    return 0;
}      

(2)向數組直接拷貝字元串:

//#include "stdafx.h"//If the vc++6.0, with this line.
#include "stdio.h"
#include "string.h"
struct stu{
    int x;
    char name[10];
};

int main(void){
    struct stu s;
    strcpy(s.name,"abcd");//向name拷貝字元串
    s.x=128;
    printf("%d %s\n",s.x,s.name);
    return 0;
}      

至于為什麼不能直接給字元數組指派字元串呢?網上各大神說的是,因為在初始化字元數組時,數組的記憶體位址已經确定,不能再做修改。

想要直接給數組指派字元串除了在初始化時指派,還可以通過兩個函數,void *memcpy(void*dest, const void *src, size_t n);strcpy(str1,str2)。

原因:

(1)首先,其實是忘記了C++的基礎問題,C++裡面隻要涉及char都不能直接通過“=”來指派,因為C++裡面沒有提供這個功能。必須使用str開頭的函數。隻有後來的CString重載來“-,+,=”之後才可以怎麼友善的使用。CString str;str=“sasa";.

(2)其次,stu.name="hello";//報錯為什麼?name[10]是一個10大小的記憶體空間,而”hello“是一個常量匿名字元串的位址,現在你應該明白了.

你把一個位址指派給了數組,也就是說現在char[20]="0x51825182"之類的,get it !

總結:結構體字元數組的指派必須用字元串函數,不能直接進行指派。