C语言——数据类型

  • Post author:
  • Post category:其他

C语言目录:

1. 概述

2. 数据类型

3. 量

4. 运算符

5. 流程控制

6. 函数

7. C程序编译过程

8. 文件

9. 内存管理


2.1 空类型 NULL

2.2 基本数据类型

一个变量所占用的存储空间,和 定义变量时声明的类型 以及 编译环境 有关

类型 32位编译器 64位编译器
char 1 1
int 4 4
float 4 4
double 8 8
short 2 2
long 4 8
long long 8 8
void* 4 8

2.2.1 char

char类型变量占1个字节存储空间,共8位

除单个字符以外, C语言的的转义字符也可以利用char类型存储

字符 意义
\b 退格(BS)当前位置向后回退一个字符
\r 回车(CR),将当前位置移至本行开头
\n 换行(LF),将当前位置移至下一行开头
\t 水平制表(HT),跳到下一个 TAB 位置
\0 用于表示字符串的结束标记
\ 代表一个反斜线字符 \
\" 代表一个双引号字符”
\' 代表一个单引号字符’

ASCII码:定义了字符与01串的映射关系
AASCII

采用ASCII编码的字符占一个字节,一个中文字符(采用unicode)占三个字节

可见,char 类型变量不能存储中文内容
除转义字符外,不支持多个字符

2.2.2 int与其说明符

int 占用4字节的内存空间,取值范围为

2

31

2

31

1

-2^{31}-2^{31}-1

2312311

C语言中提供了 长度说明符符号位说明 两种类型

这些类型说明符都是用于修饰 int 的,所以可以省略 int

a. short 和 long

shortlong 可提供不同长度的整型,相应的改变整型数值的取值范围

  • short 占用2字节,取值范围

    2

    15

    2

    15

    1

    -2^{15}-2^{15}-1

    2152151

  • long 占用8字节,取值范围

    2

    63

    2

    63

    1

    -2^{63}-2^{63}-1

    2632631

short 的长度不能大于 intint 长度不能大于 long

在32bit编译器环境下,long 占用4字节, long long 占用8字节

64bit编译器环境下,longlong long 占用8字节

#include <stdio.h>

int main(){
    // char占1个字节, char的取值范围 -2^7~2^7
    char num = 129;
    // num中存储的内容:1000 0001
    // 转化为十进制 1111 1111 -127
    
    // short int 占2个字节, short int的取值范围 -2^15~2^15-1 -32768~32767
    short int num1 = 32769;// -32767
    //num1中存储的内容:1000 0000 0000 0001
    //转化为十进制 1111 1111 1111 1111 = -32767

    // 由于short/long/long long一般都是用于修饰int, 所以int可以省略
    short num2 = 123;
    printf("num5 = %d\n", num5);
    long num3 = 123;
    printf("num6 = %ld\n", num6);
    long long num4 = 123;
    printf("num7 = %lld\n", num7);
    return 0;
}

b. signed 和 unsigned

unsignedsigned 的区别在于最高位是否要当做符号位,他并不会改变 int 的存储空间

  • signed 等价于 int 等价于 signed int ,数值范围为

    2

    31

    2

    31

    1

    -2^{31}-2^{31}-1

    2312311

  • unsigned 表示无符号,最高位不当做符号位,数值范围

    0

    2

    32

    1

    0-2^{32}-1

    02321

符号位说明符和长度说明符可以混合使用

2.2.3 枚举类型

在枚举类型的定义中,列举出所有可能的取值

枚举类型变量的取值不能超过定义的范围

a. 枚举类型定义

enum 枚举名 {
	枚举元素1;
    枚举元素2;
    ......
};
enum Season {
    Spring;
    Summer;
    Autum;
    Winter;
};

b. 枚举类型变量定义

先定义枚举类型,再定义枚举变量

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
enum Season s;

定义枚举类型的同时定义枚举变量

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;

省略枚举名称,直接定义枚举变量

enum {
    Spring,
    Summer,
    Autumn,
    Winter
} s;

c. 枚举变量的赋值与使用

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
} s;

s = Spring; // 等价于 s = 0;
s = 3; // 等价于 s = winter;
printf("%d", s);
  • C编译器将枚举类型变量的元素作为整型常量处理,称为枚举常量

  • 枚举元素的值取决于定义时各枚举元素排列的先后顺序

    默认情况下,第一个枚举元素的值为0,第二个为1,递增

enum Season {
    Spring,
    Summer,
    Autumn,
    Winter
};
// 默认情况下,spring的值为0,summer的值为1,autumn的值为2,winter的值为3

在定义枚举类型时可修改枚举元素的值

enum Season {
    Spring = 9,
    Summer,
    Autumn,
    Winter
};
// spring的值为9,summer的值为10,autumn的值为11,winter的值为12

2.3 构造类型

2.3.1 结构体

用于保存一组不同类型的数据

  • 当结构体成员是其本身时,只能用指针
  • 当初始化参数列表中参数数量少于成员个数时,其余成员自动赋0
  • 对结构体成员操作,实质上是对其所在地址进行操作

a. 结构体定义

struct 结构体类型名{
    类型名1 成员名1;
    类型名2 成员名2;
    ...
    类型名3 成员名3;
};
---
struct Student{
  char *name;
  int age;
  float height;
};

b. 结构体变量

先定义结构体类型,再定义结构体变量名

struct Student {
    char *name;
    int age;
};

struct student stu;

定义结构体类型的同时定义变量

struct Student {
  char *name;
  int age;
} stu;

匿名结构体定义结构体变量

struct {
    char *name;
    int age;
}stu;

c. 结构体成员访问

结构体变量名.成员名

struct Student {
    char *name;
    int age;
};

struct Student stu;

// 访问stu的age成员
stu.age = 27;
printf("age = %d", stu.age);

d. 结构体成员初始化

定义的同时按顺序初始化

struct Struct {
    char *name;
    int age;
};

struct Student stu = {"Amos",23};

定义的同时不按顺序初始化

struct Struct {
    char *name;
    int age;
};

struct Student stu = {.age = 23, .name = "Amos"};

先定义后逐个初始化

struct Student {
    char *name;
    int age;
};

struct Student stu;
stu.name = "Amos";
stu.age = 23;

先定义后一次性初始化

struct Student {
     char *name;
     int age;
 };
struct Student stu;
stu = (struct Student){"Amos", 23};

e. 结构体作用域

结构体类型定义在函数内部的作用域与局部变量的作用域相同

  • 从定义的那一行开始,直到遇到 return 或者 {} 为止
  • 函数内部的结构体会屏蔽同名的全局结构体

结构体类型定义在函数外部的作用域与全局变量的作用域相同

  • 从定义的哪一行开始,知道本文件结束为止

f. 结构体数组

struct 结构体类型名称 数组名称[元素个数];

struct Student {
	char *name;
    int age;
};

struct Student stu[2];
初始化

定义同时初始化

struct Student {
    char *name;
    int age;
};
struct Student stu[2] = {{"Amos", 23},{"zz", 18}}; 

先定义后初始化

struct Student {
    char *name;
    int age;
};
struct Student stu[2];
stu[0] = {"Amos", 23};
stu[1] = {"zz", 18}; 

g. 结构体指针

一个指针变量当用指向一个结构体变量时,称之为结构体指针变量

struct 结构名 *结构指针变量名;

struct Student {
    char *name;
    int age;
};

// 定义一个结构体变量
struct Student stu = {"Amos", 23};
//定义一个指向结构体的指针变量
struct Student *p;
p =  &stu;

# 访问成员变量的三种方法
printf("name=%s, age = %d \n", stu.name, stu.age);
printf("name=%s, age = %d \n", (*p).name, (*p).age);
printf("name=%s, age = %d \n", p->name, p->age);

h. 结构体内存分析

给结构体变量开辟内存空间和给普通变量开辟内存空间一样,从内存的大地之开始,向小地址扩展

给结构体成员开辟内存空间和给数组元素开辟存储空间一样,会从给该变量分配的内存地址小的位置开始

结构体变量占用的内存空间是所有成员类型中最大内存占用的倍数

struct Person{
    int age; // 4
    char ch; // 1
    double score; // 8
};
struct Person p;
printf("sizeof = %i\n", sizeof(p)); // 16
  • 占用内存最大的成员是 score ,占8个字节。所以以8字节为单位进行内存申请。
  • 第一个8字节分配给 chage ,所以一共开辟了两个8字节的存储空间
  • 第二个8字节分配给 score
struct Person{
    int age; // 4
    double score; // 8
    char ch; // 1
};
struct Person p;
printf("sizeof = %i\n", sizeof(p)); // 24
  • 占用内存最大的成员是 score ,占8个字节。所以以8字节为单位进行内存申请
  • 第一个8字节分配给 age
  • 第二个8字节分配给 score :第一个8字节只剩4字节,不够给 age 分配,需要重新申请
  • 第三个8字节分配给 ch :第二个8字节全部分配给了 score ,所以需要新开辟8个字节

i. 结构体嵌套定义

结构体成员可以是一个结构体

struct Date {
    int year;
    int mouth;
    int day;
};

struct Student {
    int num;
    char *name;
    char sex;
    struct Date birthday;
    Float score;
};

结构体不可以嵌套同类型结构体变量

# 错误写法
struct Student {
    int age;
 	struct Student stu;
};

对嵌套结构体成员的访问,连续使用成员运算符 . 访问最低一级成员

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    char *name;
    struct Date birthday;
};

struct Student stu;
stu.name = "Amos";
stu.birthday.year = 2000;
stu.birthday.month = 1;
stu.birthday.day = 1;

j. 结构体和函数

结构体之间赋值是值拷贝 ,不是地址传递

struct Person{
    char *name;
    int age;
};
struct Person p1 = {"Amos", 23};
struct Person p2;
p2 = p1;
p2.name = "Auspice"; // 修改p2不会影响p1
printf("p1.name = %s\n", p1.name); // Amos
printf("p2.name = %s\n", p2.name); //  Auspice

在函数内修改形参,不会影响外界实参

#include <stdio.h>

struct Person{
    char *name;
    int age;
};

void test(struct Person p);

int main()
{
    struct Person p1 = {"Amos", 23};
    printf("p1.name = %s\n", p1.name); // Amos
    alter(p1);
    printf("p1.name = %s\n", p1.name); // Amos
    return 0;
}
void alter(struct Person per){
    per.name = "Auspice";
}

k. 位域

特殊的结构体,以二进制位为单位,指定所占的二进制位数

  • 只能用 unsigned,int,signed int

  • 当存储单元剩下的空间不够存放一个位域时,从跳到下个存储单元,从低位开始存储

2.3.2 共用体

a. 结构体和共用体区别

结构体:用于将多种数据组合成一个整体。

共用体:采用覆盖技术,几个不同变量共享一段内存空间。成员的起始地址一样,都是共用体的起始地址,在同一时刻只能使用其中的一个成员变量

区别:

  • 结构体每一位成员表示一种具体事物的属性,共用体是多种属性的集合
  • 结构体的存储空间为各成员占用存储空间的和,共用体存储空间为最大成员的存储空间
  • 共用体不能初始化,但结构体可以

b. 共用体定义

union 共用体名 {
    数据类型 成员名称;
    数据类型 成员名称;
    ...	   ....
};

union 共用体名 共用体变量名;
union Test {
    int age;
    char ch;
};

union Test t;

t.age = 23;
printf("t.age = %d\n" ,t.age);//23
t.ch = 'a';
printf("t.ch = %c\n",t.ch);//a
printf("t.age = %d\n",t.age);//23

c. 应用场景

  1. 通信中的数据包会用到共用体,因为不知道对方会发送什么样的数据包过来
  2. 节约内存。两个很长的数据结构,但不会同时使用,用结构体就比较浪费内存
  3. 某些应用需要大量临时变量,这些变量类型不同,且随时更换。可以使用共用体让这些变量共享一个内存空间,这些临时变量不需要长期保存

2.3.3 数组

数组:一组 相同数据类型 数据的有序集合

数组名:该数组在内存中的起始地址

数组元素:构成数组的每一个数据

数组下标:数组元素下标范围:

(

0

,

元素个数

1

)

(0,元素个数-1)

(0,元素个数1)

a. 数组定义

元素名 数组名[元素个数];

  • 元素个数只能写整型常量或者常量表达式
int ages4['A'] = {19, 22, 33};
printf("ages4[0] = %d\n", ages4[0]);

int ages5[5 + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);

int ages5['A' + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);

# 错误:未指定元素个数
int a[];

# 错误:只能在定义数组的时候进行一次性(全部赋值)的初始化
int ages3[5];
ages10 = {19, 22, 33};

b. 初始化数组

没有初始化的数组,存储的是随机的垃圾值

{} 中的各数据值即为各元素的初值,各值之间用逗号间隔

定义的同时初始化

指定元素个数,完全初始化

int nums[3] = {1,2,3};

指定元素个数,部分初始化

  • 没有显式初始化的元素,系统会自动将其初始化为0

    int nums[10] = {1,2};
    
  • 可以指定部分索引的值

    int nums[5] = {[4] = 3,[1] = 2};
    

不指定元素个数,完全初始化

int nums[] = {1,2,3};
  • 不指定元素个数,部分初始化

    int nums[] = {[4] = 3};
    
先定义后单独初始化
int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
  • 对于数组,只能 定义时初始化定义后逐个赋值

    int nums[3];
    nums = {1,11,2}; // 报错
    

c. 数组的使用

通过 数组名[下标] 访问

通过首地址偏移量访问 *(数组名+n)

数组的遍历

有序地查看数组的每一个元素

int ages[4] = {19, 22, 33, 13};
for (int i = 0; i < 4; i++) {
    printf("ages[%d] = %d\n", i, ages[i]);
}

d. 数组长度的计算

数组存储空间的计算

数组存储空间

=

一个元素占用的存储空间

元素个数

(

数组长度

)

数组存储空间 = 一个元素占用的存储空间 * 元素个数(数组长度)

数组存储空间=一个元素占用的存储空间元素个数(数组长度)

sizeof 关键字计算数组长度
int ages[4] = {19, 22, 33, 13};
int length =  sizeof(ages)/sizeof(int);
printf("length = %d", length);
输出结果: 4

e. 数组内存分析

数组定义:从高地址开辟一块连续的没有被使用的内存给数组

数组名:指向分配给数组的最小地址

# include<cstdio>

using namespace std;

int main(int argc, char const *argv[])
{
    /* code */

    int num = 9;
    printf("num 地址%p\n",&num);

    char cs[] = {'a','b','c'};
    printf("cs 地址%p\n",cs);
    printf("cs[0] 地址%p\n",cs[0]);
    printf("cs[1] 地址%p\n",cs[1]);
    printf("cs[2] 地址%p\n",cs[2]);

    int nums[] = {2, 6};
    printf("nums = %p\n", &nums);      
    printf("nums[0] = %p\n", &nums[0]);
    printf("nums[1] = %p\n", &nums[1]);
    
    return 0;
}


  • 从该数组分配的连续存储空间中,低地址开始给元素分配一块内存
  • 每个元素采用大端模式存储数据(高字节高地址)

f. 数组越界问题

char cs1[2] = {1, 2};
char cs2[3] = {3, 4, 5};

# cs1[1] cs1[0] cs2[2] cs2[1] cs2[0]
cs2[3] = 88; // cs2[3] = cs1[0]
printf("cs1[0] = %d\n", cs1[0]);
输出结果: 88

g. 数组与函数

数组可以作为函数参数使用

把数组元素作为实参——值传递

把数组名作为函数形参和实参——地址传递

  • 实参数组名将该数组的起始地址传递给形参数组,两个数组共享一段内存单元,系统不会为形参数组分配存储单元

  • 在被调函数中修改形参数组内容,实参数组内容也会被修改

# include<stdio.h>

void change(int array[3]){
    //等价于 void change(int array[])
    // void change(int *array)
    array[0] = 88;
}

int main(int argc, const char * argv[]){
    int ages[3] = {1, 5, 8};
    printf("ages[0] = %d", ages[0]);// 1
    change(ages);
    printf("ages[0] = %d", ages[0]);// 88
}
  • 在函数形参表中,允许不给出形参数组的长度

    void change(int array[]){
        array[0] = 88;
    }
    
  • 形参数组和实参数组类型必须一致

  • 当数组名作为函数参数时,因为自动转换为指针类型,所以在函数中无法动态计算数组的元素个数

在这里插入图片描述

2.3.4 二维数组

二维数组的元素是一维数组

数据类型 数组名[一维数组个数][一维数组元素个数]

  • 一维数组的个数:包含多少一维数组
  • 一维数组的元素个数:当前二维数组的每个一维数组元素的个数

a. 初始化

定义同时初始化

int a[2][3]={ {80,75,92}, {61,65,71}};

定义后逐个赋值

int a[2][3];
a[0][0] = 80;
a[0][1] = 75;
a[0][2] = 92;
a[1][0] = 61;
a[1][1] = 65;
a[1][2] = 71;

按行分段赋值

int a[2][3]={ {80,75,92}, {61,65,71}};

指定元素初始化

int a[2][3]={[1][2]=10};
int a[2][3]={[1]={1,2,3}}

注意:行长度可以省略,但列长度不能省略

int a[][3]={{1,2,3},{4,5,6}};
int a[][3]={1,2,3,4,5,6};

//部分初始化
int a[][3]={{1},{4,5}};
int a[][3]={1,2,3,4};

b. 二维数组的遍历

char cs[2][3] = {
    {'a', 'b', 'c'},
    {'d', 'e', 'f'}
};

for(int i = 0; i < 2; i++) { // 外循环取出一维数组
    for(int j = 0;j < 3;j++) {//内循环取出一维数组的元素
        printf("%c", cs[i][j]);
    }
    printf("\n");
}

c. 二维数组的存储

从高地址开始给每个数组分配空间

从低地址开始给每个数组元素分配空间

从高地址给每个元素分配空间

每个元素大端模式存储数据

#include <stdio.h>
int main(){
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    // cs == &cs == &cs[0] == &cs[0][0]
    printf("cs = %p\n", cs);                // 0060FEAA
    printf("&cs = %p\n", &cs);              // 0060FEAA
    printf("&cs[0] = %p\n", &cs[0]);        // 0060FEAA
    printf("&cs[0][0] = %p\n", &cs[0][0]);  // 0060FEAA
    return 0;
}

在这里插入图片描述

d. 二维数组与函数

值传递
#include <stdio.h>

// 基本类型在函数中修改形参不会影响实参
void change(char ch){
    ch = 'n';
}

int main(){
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs[0][0]);
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    return 0;
}
地址传递

a[i] 表示的是一维数组首地址

#include <stdio.h>

// 数组类型在函数中修改形参会影响实参
void change(char ch[]){
    ch[0] = 'n';
}

int main(){
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs[0]);
    printf("cs[0][0] = %c\n", cs[0][0]); // n
    return 0;
}

二维数组列不能省略

#include <stdio.h>

void change(char ch[][3]){
    ch[0][0] = 'n';
}
int main(){
    char cs[2][3] = {
        {'a', 'b', 'c'},
        {'d', 'e', 'f'}
    };
    printf("cs[0][0] = %c\n", cs[0][0]); // a
    change(cs);
    printf("cs[0][0] = %c\n", cs[0][0]); // n
    return 0;
}

被调函数只知道数组的首地址,不知道行数,需要通过参数传入

可以通过 sizeof 数组名[i] 获取列数

  • sizeof(cs[0]) 获得列数
  • sizeof(cs)/sizeof(cs[0]) 获得行数

2.3.5 字符串

"" 中的字符序列表示字符串常量

以字符数组形式存储在计算机中,在每个字符串结尾需要添加一个 '\0' 表示字符串结束,\0 在ASCII中用0表示

  • 在C语言中没有专门的字符串变量,通常用一个字符数组来存放字符串

a. 初始化

char name[9] = "Amos"; 
char name1[9] = {'A','m','o','s','\0'};
char name2[9] = {'A','m','o','s',0};
// 当数组空间大于字符个数时, 未被初始化的部分是0
char name3[9] = {'A','m','o','s'};
//上述四种写法是等价的

一个字符串数组,若不做初始化赋值,必须指定数组长度

比正常数组可用的存储空间少1个,因为字符串结束标志 '\0' 需要占一个空间

b. 输入输出

使用 printf("%s",str); 进行输出

  • %s 本质是根据传入的 str 地址逐个去取数组中的元素输出(

    低地址

    高地址

    低地址\rightarrow 高地址

    低地址高地址),直至遇到 '\0'

'\0' 引发的脏读问题:

char name[] = {'c', 'o', 'o', 'l' , '\0'};
char name2[] = {'A', 'm', 'o', 's'};
printf("name2 = %s\n", name2); // 输出结果: Amoscool

name低字节与name2高字节邻接,所以从低位输出name2到高位,会将name与name2全部输出


使用 scanf("%s",str); 进行输入

char ch[10];
scanf("%s",ch);
  • 对一个字符串数组,如果不做初始化赋值, 必须指定数组长度

  • ch 最多存放由9个字符构成的字符串,其中最后一个字符的位置要留给字符串结束标示 ‘\0’

  • 当用 scanf 输入字符串时,字符串中不能有空格,否则将以空格作为串的结束符

c. 常用方法

C语言提供的字符串处理函数,大致可以分为字符串输入、输出、合并、修改、比较、替换、复制、搜索几类

  • 输入输出在 stdio.h
  • 其他方法在 string.h
输入输出

puts(字符数组名) :把字符数组中的字符串输出到显示器上

优点:

  • 自动换行
  • 可以是数组的任意元素地址

缺点

  • 不能自定义输出格式,只能输出字符串内容

在这里插入图片描述


gets(字符数组名) :从标准输入(键盘) 上输入一个字符串

char ch[30];
gets(ch); // 输入:Amos
puts(ch); // 输出:Amos

gets回车 作为输入结束,所以字符串中可以有空格

gets 的接收变量是预先定义的,所以会有数组越界的风险

字符串长度

sizeof 动态计算

字符串在内存中是字符数组的形式存储,一个字符占用一个字节

字符串的结束符也占用一个存储单元

char name[] = "666";
printf("size = %d\n", sizeof(name)); //输出结果:4
// 包括 \0

strlen(字符数组名) :获取字符串的实际长度(不包含字符结束标志 '\0'

char name[] = "666";
printf("len = %d\n", strlen(name2)); //输出结果:3

自定义

int myStrlen(char str[]){
    int cnt = 0;
    while(str[length] != '\0')
        cnt++;
    
    return cnt;
}
字符串连接

strcat(destStr,newstr);

删去 destStr 后的 \0 ,将 newStr 连接到 destStr 后,并,然后返回 destStr 的首地址

char oldStr[100] = "welcome to";
char newStr[20] = " Amos";
strcat(oldStr, newStr);
puts(oldStr); //输出: welcome to Amos"

destStr 必须定义足够的长度,否则不能全部装入被连接的字符串

字符串复制

strcpy(destSrc,srcStr); :将 srcStr 拷贝到 destSrc 中( '\0' 也会一起拷贝)

char destStr[100] = "welcome to";
char newStr[50] = " Amos";
strcpy(destStr, newStr);
puts(destStr); // 输出结果:  Amos // 原有数据会被覆盖
字符串比较

strcmp(字符数组名1,字符数组名2); :逐字符比较两个字符数组的ASCII码,并返回比较结果

  • 字符串1 == 字符串2 ,返回值 = 0
  • 字符串1 > 字符串2,返回值 > 0
  • 字符串1 < 字符串2,返回值 < 0
char bgStr[2] = "0";
char smStr[2] = "1";
printf("%d", strcmp(bgStr, smStr)); //输出结果:-1

char bgStr[2] = "1";
char smStr[2] = "1";
printf("%d", strcmp(bgStr, smStr));  //输出结果:0

char bgStr[2] = "1";
char smStr[2] = "0";
printf("%d", strcmp(bgStr, smStr)); //输出结果:1

2.4 指针类型

2.4.1 基本概念

在计算机中,所有数据都存储在内存单元,每个内存单元都有唯一对应的地址,只要通过这个地址就能找到内存单元中的数据

a. 内存地址与存储空间

内存地址:门牌号——指明当前存储空间在内存中的位置

存储空间:房间——实际放数据的地方

image-20220730113101439

一个指针就是一个内存单元的地址

变量地址:系统分配给变量的 “内存单元” 的起始地址

image-20220731104058974

b. 指针变量

在C语言中,允许用一个变量来存放其它变量的地址,即指针变量

image-20220731104058974

2.4.2 指针运算符

a. &

& 变量名:获取变量的首地址(指针)

b. *

在定义变量时,* 是一个类型说明符,说明定义的变量是一个指针变量

int *p = NULL;

在变量名前,且不是定义时,表示访问指针指向的存储空间

int a = 5;
int *p = &a;
printf("%d",*p);

在表达式中,* 表示乘法

2.4.3 指针类型

在同一编译器环境下,所有指针变量占用的内存空间是固定的

但不同类型的变量所占存储空间是不同的

指针变量:

  • 16位编译器:2字节
  • 32位编译器:4字节
  • 64位编译器:8字节

指针变量需要它所指向的数据类型告诉它要访问后续的多少个存储单元

image-20220803111940768

2.4.4 指针变量定义

指针变量:用一个变量存放其他变量的地址

int age;// 定义一个普通变量
int *pnAge; // 定义一个指针变量
pnAge = &age;

指针指向的数据类型 *指针变量名

2.4.5 指针变量的初始化

  • 指针变量只能存储地址, 不能存储其它类型
  • 指针没有初始化里面是一个垃圾值,称为 野指针
  • 指针必须初始化才可以访问其所指向存储区域

a. 定义同时初始化

int a = 5;
int *p = &a;

b. 先定义后初始化

int a = 5;
int *p;
p=&a;

c. 注意

  • 指针没有初始化,里面是一个垃圾值,称为野指针

  • 指针的指向可以改变

    int a = 5;
    int *p = &a;
    int b = 10;
    p = &b; // 修改指针指向
    
  • 多个指针变量可以指向同一个地址

2.4.6 二级指针

指针的指针,间接寻址

image-20220731110757357

char c = 'a';
char *cp;
cp = &c;
char **cp2;
cp2 = &cp;
printf("c = %c", **cp2);

2.4.7 数组的指针

获取元素地址,使用取内容符 &val【视为一维数组】

a. 数组元素的指针

一个数组由若干个数组元素组成,每个数组元素占用一段存储空间,数组元素的指针指向这段存储空间的首地址

int a[10] = {1,2,3,4,5,6,7,8,9,10};

//将数组的首元素地址赋值给指针变量 p
// 以下两种写法等价
int *p = a;
int *p = &a[0];

b. 指针访问数组元素

访问数组元素的两种方法:

  • 下标,a[i]
  • 指针,*p = a; *(p+i)

image-20220731114242203

  • 指针都可通过 p+=1;p++; 后移指向下一个存储单元,这里的 +1 指的是后移一个存储单元

  • 数组名 是一个指针,但不能被修改

    int x[10];
    x++;  //错误
    int* p = x;
    p++; //正确
    

2.4.8 字符串指针

a. 字符串定义

字符数组
char string[]=”Amos Tian”;
printf("%s\n",string);
字符串指针指向字符串
// 数组名保存的是数组第0个元素的地址, 指针也可以保存第0个元素的地址
char *str = "abc"

b. 查看字符串的某个字符

char *str = "Amos";
for(int i = 0; i < strlen(str);i++){
  printf("%c-", *(str+i)); // 输出结果:A-m-o-s
}
  • 不可修改字符串内容

    使用 字符数组 来保存的字符串是保存 里的,保存栈里面东西是可读可写,所以可以修改字符串数组中的字符

    使用 字符指针 来保存字符串,它保存的是 字符串常量地址 ,常量区是只读 的 ,所以我们不可以修改字符串中的字符

  • 未分配内存空间的指针,不能直接接收键盘输入

    # 错误写法
    char *str;
    scanf("%s", str);
    

2.4.9 指向函数的指针

函数作为一段程序,在内存中也要占据部分存储空间,这部分存储空间有一个起始地址(函数名可理解为一个函数指针),所以可以用一个指针指向这个起始地址。

a. 指向函数的指针

返回值类型 (*指针变量名)(形参1类型, 形参2类型, ...);

# include<stdio.h>

/*函数指针可指向多个函数*/
int add(int x,int y){
	return x+y;
}
int max(int x,int y){
	return x > y ? x : y;
}
int min(int x,int y){
	return x < y ? x : y;
}

int main(){
	int (*p)(int,int),addRes,a,b;
    
	p = add;
	addRes = (*p)(a,b);
	p = max;
	maxRes = (*p)(a,b);
	p = min;
	minRes = (*p)(a,b);
    
    return 0;
}

注意

  • 这类指针变量存储的是一个函数的入口地址,所以对他们加减运算无意义
  • 要求:定义指向函数的指针时,括号不能省略,否则报错:无法修改的左值
  • 约定:通过函数指针调用函数时,括号不省略,即 (函数变量名); ,只是作为一种约定,表示通过函数指针调用函数

b. 函数指针作为形参

/*函数指针可指向多个函数*/
int add(int x,int y){
	return x+y;
}

int max(int x,int y){
	return x > y ? x : y;
}

int min(int x,int y){
	return x < y ? x : y;
}

int funp(int (*p)(int,int),int x,int y){
	return (*p)(x,y);
}

int main(){
	int (*p)(int,int),res,a,b;
	p = max;
	res = funp(p,a,b);
}

2.4.10 指针数组

数据类型 *数组名[数组长度]

数组名中存的是首地址

char *str[3] = {"one","two","program"}
/=>
/*
* char *str[3];
* char str[0] = "one";
* char str[1] = "two";
* char str[2] = "program";
*/

2.5 类型转换

2.5.1 自动类型转换

a. 算数转换

在同一个算数表达式中,有多个数据类型:
先将占用内存较少的类型转换为占用内存高的类型,然后再参与运算

// 当前表达式中1.0占用8个字节, 2占用4个字节
// 所以会先将int类型的2转换为double类型,然后再计算
double b = 1.0 / 2;//0.5

b. 赋值类型转换

右侧表达式运算结果自动转换为左侧表达式类型

// 赋值时左边是什么类型,就会自动将右边转换为什么类型
int a = 10.6; //a=10

2.5.2 强制类型转换

double a = (double)(1 / 2);
// 结果为0, 因为参与运算的都是整型

double b = (double)1 / 2;
// 结果为0.5, 因为1被强制转换为了double类型, 2也会被自动提升为double类型

类型转换并不会影响到原有变量的值

# include <stdio.h>
int main(){
    double d = 3.14;
    int num = (int)d;
    printf("num = %d\n", num); // 3
    printf("d = %lf\n", d); // 3.140000
}