初級指標
本篇主要介紹:指標和變數的關系、指標型別、指標的運算子、空指標和野指標、指標和陣列、指標和字串、const 和指標、以及gdb 除錯段錯誤,
基礎概念
指標是一種特殊的變數,存放地址的變數就是指標,
int num = 1; 會申請4個位元組的記憶體來存放數字1,每次訪問 num 就是訪問這4個位元組,
訪問記憶體中的這4個位元組,不僅可以通過名稱(例如 num),還可以通過地址,
Tip:& 不僅是位運算子,還是取地址運算子,例如 int* ptr = #,就是取變數 num 的地址并將其保存到指標變數 ptr 中
請看示例:
#include <stdio.h>
int main() {
int num = 10;
// num 的地址:0x7fff4dbf01d8
printf("num 的地址:%p\n", &num);
// num 的地址加1 :0x7fff4dbf01dc,
printf("num 的地址加1 :%p\n", &num + 1);
// j 存放連續記憶體的第一個位元組地址
int *j = #
// 10,通過地址訪問
printf("%d", *j);
return 0;
}
&num 和 &num + 1 相差4個位元組,說明 &num 表示整數,
普通變數存放值,而指標用于存放地址,
通過 int *j = &num 將變數num的首地址給到指標 j(j的型別是 int *),最后通過地址(*j) 訪問整數1,
int *j 是一個int型別的指標,還有 char、float等指標型別,指標型別必須匹配,比如將 j 的指標型別換成 char,則會警告,就像這樣:
- int *j = #
+ char *j = #
運行:
/workspace/CProject-test/main.c:12:11: warning: incompatible pointer types initializing 'char *' with an expression of type 'int *' [-Wincompatible-pointer-types]
char *j = #
^ ~~~~
1 warning generated.
num 的地址:0x7ffddcfe5328
num 的地址加1 :0x7ffddcfe532c
10
Tip: 指標 j 也有地址,也就是指標的指標,現在不研究
練習
題目:請問輸出什么?
#include <stdio.h>
int main() {
int num = 10;
int *p = #
printf("用指標訪問資料 num :%d\n", *p);
*p = 11;
printf("用過指標修改 num 資料:%d\n", num);
return 0;
}
提示:資料可以通過變數訪問,也能使用地址(指標)訪問,就像通知同學去嵌入式實驗室上課,或者是 303 上課,其中*p = 11; 等價于 num = 11;
輸出:
用指標訪問資料 num :10
用過指標修改 num 資料:11
星號的作用
指標 * 有兩個主要作用(根據* 前面有無型別做區分):
- 指標型別宣告
- 取值(又稱解參考運算子),例如,*ptr 表示獲取指標變數 ptr 所指向記憶體地址上的值,
請看示例:
#include <stdio.h>
int main() {
int num = 10;
// 指標型別宣告
int *p = #
// 取值
printf("%d\n", *p); // 10
// 取值
*p = 11;
printf("%d\n", num); // 11
return 0;
}
指標型別
所占位元組
在32位系統上,指標通常占用4個位元組;而在64位系統上,指標通常占用8個位元組,請看示例:
#include <stdio.h>
int main() {
printf("char型別指標所占位元組數為:%zu\n", sizeof(char*));
printf("short型別指標所占位元組數為:%zu\n", sizeof(short*));
printf("int型別指標所占位元組數為:%zu\n", sizeof(int*));
printf("long型別指標所占位元組數為:%zu\n", sizeof(long*));
printf("float型別指標所占位元組數為:%zu\n", sizeof(float*));
printf("double型別指標所占位元組數為:%zu\n", sizeof(double*));
printf("long long型別指標所占位元組數為:%zu\n", sizeof(long long*));
return 0;
}
輸出:
char型別指標所占位元組數為:8
short型別指標所占位元組數為:8
int型別指標所占位元組數為:8
long型別指標所占位元組數為:8
float型別指標所占位元組數為:8
double型別指標所占位元組數為:8
long long型別指標所占位元組數為:8
練習
題目:請問整數型別的指標和字符型別的指標加1分別是幾個位元組?
#include <stdio.h>
int main() {
int num = 10;
printf("num 的地址:%p\n", &num);
printf("num 的地址加1 :%p\n", &num + 1);
char ch = 'a';
printf("ch 的地址:%p\n", &ch);
printf("ch 的地址加1 :%p\n", &ch + 1);
return 0;
}
輸出:
num 的地址:0x7fffe8244288
num 的地址加1 :0x7fffe824428c
ch 的地址:0x7fffe8244287
ch 的地址加1 :0x7fffe8244288
答案:int * 加1是4個位元組;char * 加1是1個位元組,&num 和 &ch 分別代表該變數的全部位元組,
指標交換資料
比如這段代碼是不能實作 a、b 兩數交換,請看示例:
#include <stdio.h>
void swap(x, y){
int tmp = x;
x = y;
y = tmp;
}
int main() {
int a = 1;
int b = 2;
swap(a, b);
printf("a:%d\n", a);
printf("b:%d\n", b);
return 0;
}
a:1
b:2
分析:呼叫 swap(a, b) 這里是一個值傳遞,找到函式入口地址,對引數 x、y 申請空間和賦值,通過 tmp 變數完成了 x和y的交換,最后回收區域變數 x、y和tmp,釋放空間,而 a,b資料沒有變化,
可以通過指標來實作兩數的交換,請看示例:
#include <stdio.h>
void swap(int* x, int* y){
int tmp = *x;
*x = *y;
*y = tmp;
}
int main() {
int a = 1;
int b = 2;
swap(&a, &b);
printf("a:%d\n", a);
printf("b:%d\n", b);
return 0;
}
a:2
b:1
分析:通過 swap(&a, &b) 將 a b 的地址傳給 x 和 y,通過 x 和 y 指標對 a 和 b 進行交換,雖然最后會銷毀swap中的區域變數,但 a 和 b的值已經完成了交換,
指標的運算子
指標和變數的關系
練習1
題目:輸出什么?
#include <stdio.h>
int main() {
int a = 10, *pa = &a, *pb;
printf("%d\n", *pa);
pb = pa;
printf("%d\n", *pb);
return 0;
}
輸出:10 10
分析:
int a = 10,
// pa 指向變數 a
*pa = &a,
// 定義一個整數型的指標 pb
*pb;
printf("%d\n", *pa);
// pb 也指向變數 a
pb = pa;
printf("%d\n", *pb);
return 0;
練習2
題目:輸出什么?
#include <stdio.h>
int main() {
int x = 3, y = 0, *px = &x;
y = *px + 5;
printf("%d\n", y);
y= ++*px;
printf("%d\n", y);
printf("%p\n", px);
y = *px++;
printf("%p\n", px);
printf("%d\n", y);
return 0;
}
輸出:
8
4
0x7ffc48b9be38
0x7ffc48b9be3c
4
分析:
y= ++*px;等效++(*px),如果是 ++* 是不對的
類似 y = ++i,等于先執行 ++,在執行 y = i,
這里先對 (*px) 執行 ++,在回傳 *px 的值
y = *px++;
先執行 y = *px,然后是 px++,px是整數型別的地址,加1就是加4個位元組,
練習3
題目:輸出什么?
#include <stdio.h>
int main() {
int x = 3, y = 0, *px = &x;
printf("%p\n", px);
y = (*px)++;
printf("%p\n", px);
printf("%d\n", x);
return 0;
}
輸出:
0x7ffef1dc4d58
0x7ffef1dc4d58
4
分析:*px++ 表示指標加1,(*px)++ 表示值加1,
指標初始化
指標初始化有兩種方法:已經存在的空間和自己申請空間,
已經存在的空間,例如:
#include <stdio.h>
#include <stdlib.h>
int main() {
int num;
int* p = #
*p = 10;
char *str = "abc";
printf("%s\n", str); // abc,把字串的地址賦值給指標變數
return 0;
}
自己申請空間可以使用 malloc 函式,申請的是 void 型別指標,也稱為通用型別指標,請看示例:
#include <stdio.h>
// malloc 需要引入 <stdlib.h>
#include <stdlib.h>
int main() {
// 申請16個位元組
int* q = malloc(sizeof(int) * 4); // 在堆里申請了16個位元組
// int* q = (int *)malloc(sizeof(int) * 4); // 推薦
*q = 10;
// 釋放申請的16個位元組
free(q);
return 0;
}
申請空間,使用完需要使用 free() 釋放,
Tip:根據 C99 標準以及更高版本的標準,顯式的型別轉換是建議的做法,以確保型別的安全性和可讀性,
空指標和野指標
下面這段代碼 p 就是一個野指標,運行報錯:段錯誤 (核心已轉儲):
#include <stdio.h>
int main() {
int* p;
*p = 1;
return 0;
}
這里宣告一個指標 p,里面是一個亂數,例如 0x7ffe71df3f40,接著往指向的記憶體放1,由于這塊記憶體不知道是否存在,即使存在也不能訪問,于是報段錯誤,
直接手寫一個地址也不可以,就像這樣:
#include <stdio.h>
int main() {
// warning: incompatible integer to pointer conversion initializing 'int *' with an expression of type 'long' [-Wint-conversion]
// 這個警告是因為你正在將一個 long 型別的運算式賦值給一個 int* 型別的指標變數,導致型別不匹配,
// int* p = 0x7ffe71df3f40;
int* p = (int *)0x7ffe71df3f40;
*p = 100;
return 0;
}
// 分段錯誤 (核心已轉儲)"
Segmentation fault (core dumped)
空指標也不能使用:
int* p = NULL;
*p = 100;
// 輸出:`Segmentation fault (core dumped)`
但空指標會讓你可控,就像這樣:
int* p = NULL;
if (p != NULL) {
printf("p is not NULL\n");
}else{
printf("p is NULL\n");
}
// 輸出:p is NULL
指標和陣列
指標當陣列用
遍歷一個陣列,可以這樣:
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]); // 計算陣列的長度
// 1 2 3 4 5
for (int i = 0; i < length; ++i) {
printf("%d ", arr[i]);
}
return 0;
}
使用指標遍歷陣列有兩種方式(效果相同),請看示例:
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int length = sizeof(arr) / sizeof(arr[0]); // 計算陣列的長度
// 指標遍歷方式1
/*
int* pArr = arr;
for (int i = 0; i < length; ++i) {
printf("%d ", *(pArr + i));
}
*/
// 指標遍歷方式2
int* pArr = arr;
for (int i = 0; i < length; ++i) {
printf("%d ", pArr[i]);
}
return 0;
}
Tip:在陣列一文中我們知道陣列名表示首元素地址,這里*(pArr + i)會依次遍歷陣列或許是因為指標是int型別吧!
總結:pArr[i] 等于 *(pArr + i),在這里[]不再是取某個索引,而是表示取值,
指標和字符陣列
題目:分析 char a[] = "Hello"; 和 char *b = "World";
- 都可以用for遍歷元素,例如:
#include <stdio.h>
int main() {
char a[] = "Hello";
char *b = "World";
// Iterating over 'a'
printf("Characters in 'a':\n");
for (int i = 0; a[i] != '\0'; i++) {
printf("%c\n", a[i]);
}
// Iterating over 'b'
printf("\nCharacters in 'b':\n");
for (int i = 0; b[i] != '\0'; i++) {
printf("%c\n", b[i]);
}
return 0;
}
輸出:
開始運行...
Characters in 'a':
H
e
l
l
o
Characters in 'b':
W
o
r
l
d
運行結束,
- 為什么指標也可以通過索引訪問特定字符?
比如char *b = "World";,可以將字串視為字符陣列,使用指標來指向該陣列的首地址,指標可以通過偏移來訪問特定位置的元素,包括字串中的字符,
練習
題目:下面代碼中 p1[0]、p2[0]、p3[0]的值分別是多少?
// 申請4*4個位元組,每個位元組地址假如是:0x100(存放1) 0x104(存放2) 0x108 0x10c
int a[] = {1,2,3,4};
int *p1 = (int*)(&a + 1);
int *p2 = (int*)((int)a + 1);
int *p3 = (int*)(a + 1);
分析:
(int*)(&a + 1)- &a 表示整個陣列,加1則到下一個陣列,然后將陣列指標強轉成整數指標,指向第5個元素,其實已經越界了,(int*)((int)a + 1)- a 表示陣列首元素地址,(int)a 將地址轉為整數,以前是加1個元素,現在就是加1,然后又將整數轉為整數指標,亂了(就好比訪問0x101 0x102 0x103 0x104)(int*)(a + 1)- a 表示陣列首元素地址,加1則是第二個元素地址 0x104,不強轉也可以,
結論:只有p3[0](等價于 *(p3 + 0))是一個正常的元素,也就是2.
指標和字串
題目:用陣列和指標定義字串有什么區別?
#include <stdio.h>
int main() {
char str[] = "HelloWorld";
// HelloWorld
printf("%s\n", str);
char* s = "HelloWorld";
// HelloWorld
printf("%s\n", s);
return 0;
}
Tip: 字串的輸出都是首地址,比如這里的 str 是陣列的首地址,s 指標指向的也是首地址,
分析:
char str[] = "HelloWorld"; 在堆疊中定義一個陣列,用11個位元組存盤HelloWorld(還有一個 \0),請看示例:
#include <stdio.h>
int main() {
char str[] = "HelloWorld";
str[0]++;
// IelloWorld
printf("%s\n", str);
// error: cannot increment value of type 'char[11]'
str++;
// printf("%s\n", str);
return 0;
}
陣列名(str++)不可以修改,str 就是陣列首元素地址,已經固定了,可認為它是常量,但陣列內容可以修改,
char* s = "helloWorld"; 將 helloWorld 放在只讀資料區,s 是區域變數,放在堆疊中,占8個位元組,請看示例:
#include <stdio.h>
int main() {
char* s = "helloWorld";
s++;
// elloWorld
printf("%s\n", s);
// 報錯:Segmentation fault (core dumped)
s[0]++;
return 0;
}
指標可以加加,但指標指向的內容不能修改,
str 只是個名字,不占空間,如果一定要說占多少,那就是它執行的陣列占11個位元組,而 s 是8個位元組,指向一個只讀區,占 11 個位元組,
練習
題目:分析以下示例,
#include <stdio.h>
int main() {
char str[20];
str = "HelloWorld";
char* s;
s = "HelloWorld";
// HelloWorld
printf("%s\n", s);
return 0;
}
分析:
// 分配20個位元組的記憶體,并把首地址給 str
char str[20];
// str 是只讀的,不能再賦值,報錯:`error: array type 'char[20]' is not assignable`
str = "HelloWorld";
// 定義一個指標 s
char* s;
// 將 HelloWorld 的首地址給 s
s = "HelloWorld";
擴展
自定義strcpy()函式
題目:實作原生字串拷貝方法strcpy,strcpy 其用法如下:
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello";
char destination[10]; // 目標字串需要足夠的空間來容納 source 字串
strcpy(destination, source);
printf("Source string: %s\n", source);
printf("Destination string: %s\n", destination);
return 0;
}
實作:
#include <stdio.h>
char* strcpy_custom(char* destination, const char* source) {
// 字串陣列末尾有一個特殊的空字符 '\0' 來表示字串的結束,逐個復制字符,直到遇到源字串的結束標志 '\0'
while (*source != '\0') {
*destination = *source;
destination++;
source++;
}
*destination = '\0'; // 在目標字串末尾添加結束標志 '\0'
return destination;
}
int main() {
// 定義兩個字符陣列
char source[] = "Hello";
char destination[10]; // 目標字串需要足夠的空間來容納 source 字串
// 陣列名,表示首元素的地址,加 1 是加一個元素(比如這里1個位元組)
strcpy_custom(destination, source);
printf("Source string: %s\n", source);
printf("Destination string: %s\n", destination);
return 0;
}
Tip:const char* source 中 const 的作用請看const 和指標
輸出:
開始運行...
Source string: Hello
Destination string: Hello
運行結束,
將 while 替換成下面一行代碼效果也相同:
char* strcpy_custom(char* destination, const char* source) {
/*
while (*source != '\0') {
*destination = *source;
destination++;
source++;
}
*destination = '\0';
*/
// 替換成
while((*destination++ = *source++) != '\0');
return destination;
}
分析:(*destination++ = *source++) != '\0':
之前的是首先判斷,在賦值,`*source != '\0'`、`*destination = '\0';`,這里是先賦值
后置++會放在運算式最后,所以等于:
(*destination = *source) != '\0';
destination++;
source++;
const 和指標
首先補充下(int*)的作用,之前說到 const 定義的變數可以被修改,我們寫了如下代碼:
#include <stdio.h>
int main() {
const int val =5;
int *ptr= (int*)&val;
*ptr=10;
printf("val = %d\n",val);
printf("*ptr = %d\n", *ptr);
return 0;
}
其中 int *ptr= (int*)&val; 是將一個 const int 型別的變數 val 地址強制轉換為 int* 型別的指標,并將指標存盤在 ptr 中,這種型別轉換是不安全的,因為它丟失了 val 的常量性質,
const char* source 宣告一個常量指標,以下代碼僅做示意:
#include <stdio.h>
int main() {
const char* source = "Hello";
char* mutableSource = "World";
printf("%c\n", source[0]);
printf("%c\n", mutableSource[0]);
// 以下操作是非法的,會導致編譯錯誤
// source[0] = 'h'; // 不能修改字符資料
// 合法
// 盡管mutableSource是一個非常量指標,看起來可以進行修改,但修改字串常量是不被允許的,并且這可能導致未定義行為,
mutableSource[4] = 'w'; // 可以修改字符資料
return 0;
}
運行:
開始運行...
H
W
Segmentation fault (core dumped)
運行結束,
就近原則
const 有個就近原則:
- 比如:
const int* p1 = #,const 修飾的是 *,所以 *p1 不能修改, p1 可以修改 - 比如:
int* const p2 = #,const 修飾 p2,所以 p2 不能修改,*p2 可以修改
請看示例:
#include <stdio.h>
int main() {
int num = 1;
const int* p1 = # // const 修飾的是 *,所以 *p1 不能修改, p1 可以修改
p1++;
// (*p1)++;
int* const p2 = # // const 修飾 p2,所以 p2 不能修改,*p2 可以修改
// p2++;
(*p2)++;
const int* const p3 = # // 兩個都不能修改
// p3++;
// (*p3)++;
return 0;
}
gdb 除錯段錯誤
GDB(GNU Debugger)是一款強大的除錯器,用于幫助開發者查找和解決程式中的錯誤,通過與源代碼互動,并提供諸如斷點設定、變數觀察、記憶體檢查等功能,GDB允許開發者逐行執行程式并分析其運行狀態,
除了上文使用的 run,還有如下操作
- run:運行程式,
- break
<line_number>:在指定行設定斷點, - break
<function_name>:在指定函式設定斷點, - continue:繼續執行程式直到下一個斷點或程式結束,
- next:逐程序地執行程式,
- step:逐陳述句地執行程式,
- print
<variable>:列印變數的值, - backtrace:顯示函式呼叫的堆疊跟蹤資訊,
- quit:退出GDB除錯會話,
使用 gdb 除錯段錯誤的程序如下:
撰寫代碼:
pjl@pjl-pc:~/pjl$ cat demo-3.c
#include <stdio.h>
int main() {
int* p;
*p = 1;
return 0;
}
編譯運行發現段錯誤:
pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3
pjl@pjl-pc:~/pjl$ ./demo-3
段錯誤 (核心已轉儲)
將代碼編譯為可除錯的可執行檔案,在gcc或g++編譯時,添加"-g"選項可以生成包含除錯資訊的可執行檔案,
// 增加 -g
pjl@pjl-pc:~/pjl$ gcc demo-3.c -o demo-3 -g
// 啟動GDB并加載可執行檔案
pjl@pjl-pc:~/pjl$ gdb demo-3
GNU gdb (Ubuntu 9.1-0kylin1) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from demo-3...
(gdb)
輸入 run(還有其他操作) 找到是第5行代碼報錯:
...
// run:運行程式,
(gdb) run
Starting program: /home/pjl/pjl/demo-3
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555135 in main () at demo-3.c:5
5 *p = 1;
(gdb)
高級指標
提前透露:指標遇上陣列
題目:以下代碼輸出什么?
#include <stdio.h>
int main() {
char * string[] = {"Hello", "World" };
printf("%s\n", string);
return 0;
}
分析:
我們知道定義字串有以下兩種方法:
char str[] = "HelloWorld";
char* s = "HelloWorld";
Tip: string 在 C 中不是關鍵字,也不是保留字,就是一個普通變數名,
[] 的優先級是非常高的,這里首先是定義一個陣列(string[]),其次就是指標,合起來就是一個指標陣列,
首先在只讀區分配兩塊記憶體分別存放 Hello(地址比如是 0x100) 和 World(地址比如是 0x200),指標陣列是16個位元組,本質就是陣列,只不過里面放的是指標,比如前8個位元組的地址是0x1000,那么 string 就是 0x1000,因為陣列名就是陣列首元素地址,
所以要輸出這兩個字串,可以這么寫:
#include <stdio.h>
int main() {
char * string[] = {"Hello", "World" };
// Hello
printf("%s\n", string[0]);
// World
printf("%s\n", string[1]);
return 0;
}
出處:https://www.cnblogs.com/pengjiali/p/17502968.html
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555910.html
標籤:其他
上一篇:PGO前瞻
下一篇:返回列表
