C语言-数据类型

0. 概念

  • 基本类型
  • 枚举类型

只能被赋值一定的离散整数值变量

  • void类型

类型说明符,表明没有可用的值

  • 派生类型

指针类型、数组类型、结构类型、共用体类型和函数类型
其中:函数类型指的是函数返回值的类型

0.1 表达式与语句

表达式必须返回一个结果
语句可以没有结果

表达式

表达式 由运算符操作数构成
表达式 也可仅由操作数构成 : 这种最常见的就是常量表达式 :4(字面值) ‘A’(字符) “abc”(字符串)
每个表达式都有一个返回值
常量表达式的返回值就是其本身

4 
4+21 // 运算表达式,也是常量表达式
a=5+2 // 赋值表达式,不是常量表达式
a>3 // 关系表达式,不是常量表达式

0.1.1 字面量常量表达式/表达式

字面量:不能改变的值,数字、字符、字符串
字面值类型:算数类型、引用和指针
常量表达式值不会改变且在编译过程中就能得到计算结果的表达式字面量属于常量表达式

int a=1+2; // 不是常量表达式,是赋值表达式
const int a=1+1; // 是常量表达式,但是a不能用在switch case中
const int b=a+1; // 是常量表达式,但是不能用在switch case中
const int a=func(); // 不是常量表达式,这个需要在程序运行的时候获取

0.2 变量/常量

  • 对于不带初始化的定义:带有静态存储持续时间的变量会被隐式初始化为NULL(所有字节的值都是0),其他所有变量的初始值是未定义的
  • 需要建立存储空间的变量变量在声明的时候就已经建立了存储空间
  • 无需建立存储空间的变量:通过extern关键字声明变量名,而不定义它,这种可以在别的文件中定义

变量定义就是告诉编译器在何处创建变量的存储,以及如何创建变量的存储
声明与定义
extern int i 是声明,不是定义
int a 是声明,也是定义

0.2.1 变量作用域

main函数也是函数,main函数中定义的变量也是局部变量

  • 在函数或块内部的局部变量

在某个函数或块内部声明的变量称为局部变量,只能被函数或该代码块内部的语句使用

  • 在所有函数外部的全局变量

全局变量在整个声明周期内都是有效的,在任意的函数内部都能访问全局变量

  • 形参

被当做函数内的局部变量

命名冲突

局部作用域内,优先使用局部变量

0.2.1.1 全局变量与局部变量

全局变量:保存在内存的全局存储区中,占用静态的存储单元
局部变量:保存在中,只有在所在函数被调用时才动态的为变量分配存储单元

自动分配存储空间
局部变量、形参、返回值、const局部变量

手动开辟释放 malloc/free new/delete

  • 全局/静态区

全局变量、静态全局变量、静态局部变量

  • 常量/静态区

字符、字面值、字符串等、const 修饰的全局变量

局部变量和全局变量的初始化

局部变量被定义时,系统不会对其初始化(或随机初始化,根据编译器的不同策略不同),必须手动对其初始化
全局变量被定义时,系统会自动对其初始化,默认是0,(因为全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会初始化为0)
在函数内部修改全局变量会影响其他函数,

int 0char-\0float 0double 0pointer NULL

未初始化的变量会导致一些在内存位置中已经可用的垃圾值

0.2.1.2 作用域扩展 python
a=1
b=2
c=4
def f():
    print(c)
    c=a+b
f()
print(c)
# c为局部变量,会报错!!!

1. 整型

  • int

2或4字节,(win 4字节)

  • unsigned int

2或4字节

  • short/unsigned short

2字节

  • long/unsigned long

4字节

  • long long

8字节
超过2^31-1的初始值,要加上LL后缀

  • double

8字节

  • char

1个字节,[-128,127] 或[0,255]

  • unsigned char

1个字节,[0,255]

  • size_t

在 stdio.h / stdlib.h 头文件中使用typedef定义的数据类型,表示无符号整数,也是非负数,常用来表示整数

前缀:表示进制关系
后缀:表示有符号(默认)和无符号整型U、长整型L,大小写任意,顺序任意

1.1 有符号/无符号

当以有符号数的形式输出时,printf会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位,但是对于有符号的正数而言,最高位恰好是0,所以可以按照无符号数的形式读取,且不会有任何的数值影响
当以无符号的形式输出时,printf也会读取数字所占用的内存,并把所有的内存都作为数值位对待

1.2 进制数

0b 0B # 二进制数 
0x 0X # 十六进制数
0 #八进制

注意标准编译器不支持0b的二进制写法,只是某些编译器在扩展后支持
注意printf 输出形式

整型只控制字节长度,和数值范围
进制数才是实际数值的表现形式
一个数字不管以何种进制来表示,都可以 以任意进制的形式输出,因为数字在内存中始终以二进制的形式存储
其他进制的数字在存储前都必须转换成二进制形式,而在输出前也需要反转

1.3 原码/补码/反码

原码:数值的二进制数,包括符号位
反码:

正数的反码=原码
负数的反码=符号位不动,其他位置取反

补码:

正数的补码=原码
负数的补码:是反码加1

-128的补码就是1000 0000(-128没有原码和补码)
补码 1000 0000 就表示-128 记住就可以了
深究里边,是因为符号位会被数值的高位进行多次覆盖
char a=0x80 // -128
int a=0x80 // 128
char a=0xff // -1
int a=0xff // 127

计算机内存中,整型一律采用补码的形式存储,因此读取整数时还要采取逆向的转换,也就是将补码转换为原码

1.4 数值溢出

数值范围 可以想象成一个
超出范围就从环上找数

1.5 float/double

  • float

4字节,32位

  • double

8字节,64位
对于小数默认是double类型

  • long double

16字节

float x=1.2 # 赋值的时候,会先从double类型转换为float类型

# 后缀
float x=1.2f # 赋值的时候直接是默认的float类型

注意所有的浮点型都是以双精度double进行运算的,即使是float类型,也要先转换成double类型

<float.h>定义了宏,可以在程序中使用这些值和其他有关实数二进制表示的细节

#include <float.h>

FLT_MIN # 最大值
FLT_MAX # 最小值
FLT_DIG # 精度值

1.6 bool 类型

c 语言一般没有bool类型

#include <stdbool.h>
bool flag=true;

2. 字符型

单引号
charunsigned char 是一个字符类型,用来存放字符,但是同时也是一个整数类型,也可以用来存放整数!!!,注意取值范围
char 1个字节,[-128,127]
unsigned char 1个字节,[0,255]
char 类型只能存储ASCII字符,不能存储其他字符根据上面的定义,也可以存放范围以内的进制数

2.1 字符型与整型

由于char类型只能存储ASCII字符,字符集及其编码其实本质上就是二进制数,(本质上与整型没有区别)
定义一个char类型字符,会先转换成ASCII字符对应的编码再存储
而存储一个整型的时候,不需要任何转换直接存储
由于数据类型不同,所以需要的字节位数不同,因此在存储和读取的时候占用的内存不一样,所以在读取 字符型和整型 的时候并不会有冲突
字符和整型可以互相赋值

char a=10; // 相当于整型转char型,截取低位
int b='a';

2.1.1 字符与进制数/字符型进制数

字符型与进制数

由于char可以存放整型,所以可以声明和定义取值范围内的进制数
下面的例子中,就可以把char想象成整型(但是要注意取值范围)

char a=0x32 // 这相当于将整型赋值给一个字符类型 (补码形式赋值)
printf("%c",a) // 2
printf("%d",a) // 50 这是因为由十六进制转换为的十进制 

// 与python类别
// '2'.encode().hex() # 32
// int('32',16) # 50
// chr(50) # '2'

字符型进制数

对于char类型的字符,(由于是窄字符,且只占一个字节,所以可以使用ASCII编码),(又由于ASCII编码的结果就是一个二进制数),可以利用转义的进制数进行表示
下面的例子,就可以把其当成字节对象(字节串对象),相当于python中的bytes对象,

char a='\x31' // a是字符数值1
char b='\x61' // b是字符a

char *str1="\x31\x32\x33\x61\x62\x63" // 字符串"123abc"

// 类别python
// '123abc'.encode().hex() # '313233616263'

2.2 putchar/getchar

putchar输出字符(只能输出单个字符)不输出\n
getchar接收字符

char a=getchar(); // 回车(输入换行符)自动读取
// 等价于 scanf("%c ",&ch); // 不能加\n

2.3 转义字符

转义字符实际占用一个字节

printf("\"string\'") // "string'

2.4 字符集与字符编码

多字节字符,也叫做窄字符变长字节存储方式,存储数据使用不同的字节数量
宽字符固定字节存储方式,存储数据使用固定的字节数量

char类型的窄字符,使用ASCII编码
char类型的窄字符串,通常来讲是UTF8编码,(其实不同的编译器在不同的平台上编码方式不同)
wchar_t类型的宽字符或宽字符串,使用UTF16或32编码

见文档

#include <wchar.h>
wchar_t a=L'a'; // 要加一个L进行标注

// 下面两个用于输出宽字符
putwchar(a);
wprintf(a);

对于中文字符应该使用宽字符的形式,并加上前缀,加上前缀后,所有的字符都变成了宽字符的存储形式
wchar_t类型,在不同的编译器下对不同的语言使用不同的长度

#include <wchar.h>
wchar_t a=L'中'

2.5 C语言字符串型

双引号
在内存中占一块连续的内存
其实字符串数据,依靠的是数组和指针来简洁的存储字符串

char s1[]="string1";
char *s2="string2"

// 初始化注意事项
// char s1[10];
// s1="hello"; // 这种不行
char *s2;
s2="world"; // 这种可以

char s1[]="string"

这种方法定义的字符串所在的内存既有读取权限又有写入权限,可以用于输入与输出函数,存储在全局数据区或栈区
[]可以指明字符串的长度,如果不指明则根据字符串自动推算
声明字符串的时候,如果没有初始化,(由于无法自动推算长度,只能手动指定)char s[10]

char *s1="string"

这种方法定义的字符串所在的内存只有读取权限,没有写入权限(不可变),只能用于输出函数,存储在常量区

对于 字符串修改
https://blog.csdn.net/qq_31347869/article/details/105877116!!!
也就是数组形式的字符串可以进行字符串修改(具有读写权限)
指针形式的字符串不可以进行修改(只有读权限)

对于字符串,整个引号中的内容作为指向该字符串存储位置的指针 !!!

// are字符串的第一个字符的地址
// 字符串可以当做指针*"family"相当于取指针指向的元素,取地址的元素,也就是该字符串的首元素
printf("%s,%p,%c\n","we","are",*"family"); // we,0x409001,f

补充!!!
字符串数组形式与指针形式的区别

数组初始化是从静态存储区把一个字符串复制给数组(也就是存储全局数据区或栈区
指针初始化只是复制字符串的地址(也就是存储在常量区

char arr1[]="hello";
char *arr2="world";

2.5.1 puts/gets

puts输出字符串,自动换行
gets接收字符串,与scanf的区别见下

scanf空格作为结束的标志,gets只把换行符作为字符串结束的标志

char s[10]; // 这种方式可读可写,char* s 可读不可写
scanf("%s",s); // 不能加\n !!! 溢出不报错,但是会有问题
    
char a[10]={0};
gets(a); // 溢出不报错但是会有问题
printf("%s\n",a);
printf("%d\n",sizeof(a)); //10
char *str="hello world";

puts(&str[6]);
puts(str+6);

2.5.2 printf

字符串类型,有两种定义方式,对应在内存中有两种 读取与写入权限
在输出时,要求字符串只需要有读取权限

char* s1="hello";
char s2[10]="world";
printf("%s\n",s1);
printf("%s\n",s2);

2.5.3 scanf

实际上,scanf 遇到空白字符就停止读取,\t \n 空格
对于字符串,都是从输入的第一个非空白字符开始

// char s1[10];
// s1="hello"; // 这种不行
char *s2;
s2="world"; // 这种可以

// 这种可以
char s1[10];
scanf("%s",s1); // 不能加\n
printf("%s\n",s1); 

//这种不行 , 没有写入权限
// char *s2;
// scanf("%s",&s2);
// printf("%s\n",s2);

char a[10]={0};
scanf("%s",a);  // 溢出能成立,但是会报错 // 不能加\n(报不报错会根据编译器的标准而定)
printf("%s\n",a);
printf("%d\n",sizeof(a)); // 只能显示开辟的内存字节数量

scanf("%9s",a); // 不是默认设定的,而是严格指定读取的字符数,无论如何溢出都只读取指定的字符数

2.6 空字符与空指针

空字符是一个char类型的字符
空指针是一个地址
二者没有任何关系

3. 数据运算

3.1 基本运算

3.1.1 + - * /

下面的例子中 由于都是int类型,所以即使是算出了float值,也会变成int类型,只不过在printf变成了对应的格式
因此必须在先前就想好结果类型

int a=1;
int b=2;
float c;
c=a/b; // 还没赋值给c的时候,就已经截断为0了
printf("%f",c); // 0.000000

重要!!!

int a=10;
int b=3;
int c=a/b;
float d=a/b;
float e=(float)a/b;
printf("%d\n",c); // 3
printf("%f\n",c); // 0.000000 读取错误

printf("%d\n",d); //0 读取错误
printf("%f\n",d); //3.000000

printf("%d\n",e); // 随机数
printf("%f\n",e); // 3.333

3.1.2 %

%取余运算,只能作用于整型,不能作用于浮点型

注意正负数,只根据%左边的数值符号决定!!

3.1.3 ++/- -

  • ++ 自增
  • -- 自减
num++; // 先运算后+1
num--; // 先运算后+1

//例子
v=num++; // v=num;num+=1

注意

连续使用自增加会出现二义性,根据编译器进行定义

y=(4+x++)+(5+x++); // 存在多种可能性

注意

#include <iostream>
using namespace std;

int main()
{
    int a=10;
    while (a--)
    {
        cout<<a<<endl; // 第一次a已经是9了!!!
    }

    // for (;a>=0;a--)
    // {
    //     cout<<a<<endl;
    // }

    return 0;
}

3.1.4 三目运算符 ? :

如果条件为真?则值为x:否则值为y
优先级:算术运算符>关系运算符>?:
结合性:从右到左

3.1.5 赋值运算

注意,连续赋值应保证等号左边为变量、右边为赋值,并且变量已经有了存储空间
定义时,连续赋值违反规定!!!

// 下面是错误的!!!
// 赋值具有右结合性,这里1赋值给c,c赋值给b,b再赋值给a
// 但是这里相当于 b,c没有定义就赋值了,所以是错误的!!!
int a=b=c=1;

// 下面是正确的
int a,b,c;
a=b=c=1;

3.1.6 关系运算符

关系运算符的结果 是 0 1
并不是bool类型

3.1.7 逻辑运算

3.1.7.1 || 或

或之前为True 后面的就不再进行判断

补充

python: 10 or 20 返回10; 0 or 20 返回20

3.1.8 逗号表达式

逗号运算符:先计算左边,再计算右边
逗号表达式的返回值是,逗号右边的值

注意 printf() 函数中的逗号

int x=1,y=2;
int z;
x=(y=3,(z=++y+2)+5);
printf("%d\n",x); //

3.1.9 运算优先级

关系运算符:同级别具有左结合性
逻辑运算符:同级别具有左结合性,!(非)具有右结合性;逻辑运算符的优先级 < 关系运算符
单目运算符是右结合性, *p++ 等价于 *(p++)

注意结合性
同级别具有左结合性
具有右结合性的运算:单目运算符、赋值运算符(单目赋值和双目赋值)、三目运算符
! > 算数运算符 > 关系运算符 > 逻辑运算符中的&& 和 || > 赋值运算符

总体运算优先级:
单目运算符> 算数运算符>关系运算符>逻辑运算符>赋值运算符
细节上优先级:
后缀> 单目运算符(+ - !(逻辑非) ~(按位反) ++ - - (type)* &(取地址) sizeof )>算数运算符(+ -* / %) > 位移运算符>关系运算符>位运算逻辑运算符>逻辑运算符>赋值运算符

3.1.10 数据类型转换

数据类型存在强制转换
原则

浮点数赋给整型:浮点数小数部分会被舍去(注意不是四舍五入)
整数赋值给浮点数,数值不变,但是会被存储到相应的浮点型变量中
所有的浮点运算都是以double类型进行的,即使运算中只有float类型,也要先转换为double类型,才能进行运算

在这里插入图片描述

强制转换

注意强制转换是临时性的,并不会影响数值本身的数据类型和值

(类型说明符)(表达式)
(int)(x+y)

3.1.11 int/char

https://blog.csdn.net/u012782268/article/details/40021887
https://blog.csdn.net/qq_53146319/article/details/124966558 重要!!!

// 数学运算
int a=10;
char b='a';
printf("%c\n",b+a); // 'k'
printf("%d\n",b+a); // 107

int 转char 截取低八位

在表达式中(只要一出现),有符号和无符号的charshort类型都自动转换为int,在需要的情况下自动转换为unsigned int
作为函数的参数被传递时,char,short会转换为int,float会被转换为double

int 与 char 赋值的时候都是要写内存中的形式 – 补码

int a=0xff;
printf("%d\n",a); // 255
char c=a;
printf("%d\n",c); // 内存中的形式 -1
char c1=0xff; // 直接写内存中的形式,进行赋值

if (-1==c1)
{
    cout<<"-1 yes"<<endl;
}
printf("%d\n",c1); // -1

char c2=0x81;
if (-127==c2)
{
    cout<<"-127 yes"<<endl;
}
printf("%d\n",c2); // -127

3.2 大小端/位运算

3.2.1 大小端

https://blog.csdn.net/Destiny_zc/article/details/120122430
(补码操作,大端显示)

数据低位

1234中34就是低位 --> 0x4d2

大端

数据的低位放在内存的高地址上(一般都是大端模式)

小端

数据的低位放在内存的低地址上

3.2.2 位运算

位运算 是根据内存中的二进制位补码)进行运算的,而不是数据的二进制形式
相当于掩码操作

  • & 与运算

用于某些位 清0,某些位保留
按位与运算用于,关闭某些位(清零)(某些位置0)、保留某些位 (补充:清零还要想到取反)
&= 0b0010 //

int a=10;
printf("%x\n",a&0xff); // 0xa, 0xff注意在内存中的存储形式0x000000ff 
printf("%d\n",a&0xff); // 10
  • | 或运算

用于将某些位 置1,或者保留某些位
打开某些位(某些位 置1)、保留某些位
|= 0b0010 // 打开为1位(从0位开始)

int a=10;
printf("%x\n",a|0xff); // 0xff
printf("%d\n",a|0xff); // 255

int a=10;
printf("%x\n",a|0xff000000); // ff00000a
printf("%d\n",a|0xff000000); // -16777206
  • ^ 异或运算

将某些二进制位取反
某些位转置

int a=10;
printf("%x\n",a^0xff000000);
  • ~ 取反

全部取反
全部位转置

  • << 左移

高位丢弃,低位补0 (根据数据位判断高低)
符号位也丢弃
相当于乘以2的n次幂

// 左移
9<<3 
  • >> 右移

低位丢弃,高位补0或1,数据的最高位是0,就补0;如果最高位是1,那么就补1(根据数据位判断高低)
对于无符号的:左侧补0,右侧丢弃
对于有符号的:看编译器是左侧补0还是补1

对于非负数,相当于除以2的n次幂

char类型的位移操作
https://blog.csdn.net/realxie/article/details/7243601
https://blog.csdn.net/lizhoufan/article/details/83782053

在表达式中(只要一出现),有符号和无符号的charshort类型都自动转换为int,在需要的情况下自动转换为unsigned int

QByteArray d;
d.resize(sizeof(int));
int a=0x1234;
memcpy(d.data(),&a,sizeof(a)); // 0x32 0x12 0x00 0x00 小端模式存储
char a1=(a>>8) & 0xff; // 0x12 以大端看位移
char a2=a & 0xff; // 0x34
char a3=(a<<8) & 0xff;

QByteArray c;
c.resize(sizeof(int));
int b=0xff;
memcpy(c.data(),&b,sizeof(b)); // 0xff 0x00 0x00 0x00
3.2.2.1 例子
  • 查看某一位的值
// 查看第三位,相当于清零某些位,保留第三位
// (data &= 0b0010 ) == 0b0010
if ((flag &= mask) == mask) 
{
    cout<<"正确"
}
  • 综合例子1
#include <iostream>
using namespace std;

int main()
{
    // 根据位运算操作,实现字符串转二进制
    int num=10;
    char num2bin[33]; // 32位,再加\0
    char *ptr = num2bin;

    for (int i = 31; i >= 0; i--)
    {
        // 取对低位
        // +'0' 数字转字符
        ptr[i] = (num & 1) + '0'; // (num & 0x00000001)
        num >>= 1; // 右移1位 
    }
    
    // version 1
    // num2bin[32]='\0';
    // cout<<num2bin<<endl;

    // version2;
    ptr[32]='\0';
    cout<<ptr<<endl;
    return 0;
}
  • 综合例子2
#include <iostream>
using namespace std;

int main()
{
    // 根据位运算操作,实现字符串转二进制
    int num=7;
    int bits=4; // 将低bits个为置为1

    // version1
    // const int bits_val=bits;

    // int mask=0;

    // while(bits--)
    // {
    //     mask |= (1<<(bits_val-1-bits)); 
    // }

    // version2
    int bitval=1;
    int mask=0;
    while(bits--)
    {
        mask |= bitval; 
        bitval<<=1;
    }

    // 指定位异或
    num^=mask;
    // cout<<hex;
    // cout<<num<<endl;

    char num2bin[33]; // 32位,再加\0
    char *ptr = num2bin;

    for (int i = 31; i >= 0; i--)
    {
        // 取对低位
        // +'0' 数字转字符
        ptr[i] = (num & 1) + '0'; // (num & 0x00000001)
        num >>= 1; // 右移1位 
    }
    
    // version 1
    // num2bin[32]='\0';
    // cout<<num2bin<<endl;

    // version2;
    ptr[32]='\0';
    cout<<ptr<<endl;

    return 0;
}
3.2.2.1 位字段 – 还没看

void 类型

函数返回为空
函数参数为空,不接受任何参数,int main(void)
指针指向void:类型为void*的指针 代表对象的地址,而不是类型

4. I/O

4.1 printf

https://www.runoob.com/cprogramming/c-function-printf.html – 没看完
http://c.biancheng.net/view/1793.html?from=pdf_website_1_0 – 没看

%

输出格式
%[flag][width][.percision]type
width 控制输出宽度,数值不足宽度,空格补齐;数值或字符超过宽度,width不再起作用,按照数据本身宽度来输出;(例如%12f默认保留6位小数,要输出12个字符宽度);width可以用于数值、字符、字符串
.precision 控制输出精度,小数位数大于precision,按照四舍五入输出;当小数位数不足precision后面补0
.precsion 与width不同,作用于整型的时候,不足宽度在左侧补0(作用于浮点数只控制小数位数);对于控制字符串输出,字符串长度大于.precision则被截断
总结,width要么补齐空格,要么不起作用;.precision对整型左侧补0或不起作用,对于浮点小数位补0或四舍五入,对于字符串被截断或不起作用

%d //十进制有符号整数
%u //十进制无符号整数
%f //float浮点数,默认保留六位小数,不足六位以0补齐,超过六位按四舍五入截断
%lf //double
%c //字符
%s //字符串
%p //指针的值
%e //指数形式的值
%x //十六进制 无符号

%lu //32位无符号整数

整型、短整型、长整型,分别有对应的printf输出形式
printf中不能输出二进制数
需要某些转换函数
一个数字不管以何种进制来表示,都可以 以任意进制的形式输出,因为数字在内存中始终以二进制的形式存储
其他进制的数字在存储前都必须转换成二进制形式,而在输出前也需要反转
有符号和无符号
当以有符号数的形式输出时,printf会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位,但是对于有符号的正数而言,最高位恰好是0,所以可以按照无符号数的形式读取,且不会有任何的数值影响
当以无符号的形式输出时,printf也会读取数字所占用的内存,并把所有的内存都作为数值位对待
因为通过格式控制符进行数值输出的时候,其实并不会检查定义的数值是有符号还是无符号数,只会按照格式控制符对数值进行特定的解释
在这里插入图片描述

printf 表达式

printf 在直接输出表达式的计算结果时,存在顺序的不确定性

printf("%d,%d",x,4+x++); // 第一、第二个顺序不确定 

其他转义字符与特殊字符

"\\"%%

prinf与缓冲区

通常来说,printf执行结束后数据并没有直接输出到显示器上,而是放入了缓冲区,注意: 不同的操作系统,对于printf和缓存机制是不同的(一般是直到遇到\n才会将缓冲区的数据输出到屏幕中)
见《内存》

printf/puts

puts自动换行,printf不是自动换行

scanf

从控制台读取数据
根据下面的例子,scanf会根据地址把读取到的数据写入内存
注意:多输出时的结果,除了空格外,必须严格输入
注意:超额输入的时候,不会出错,相当于位置参数(这是因为缓冲区的存在
输入可以不是指定的类型,但是会存在数值转换的风险

scanf() 函数的返回值0或1
scanf() 函数的返回值 是成功接收存储数据的个数

while (scanf("%d",&num)==1)
{
    /*code*/
}

scanf与缓冲区

从键盘输入的数据并没有直接交给scanf,而是放入了缓冲区中,直到回车,scanf才到缓冲区中读取数据

int a;
scanf("输入图像",&a)

// 多输入
int a,b;
scanf("%d sb %d",&a,&b) // 输入的时候,必须是'1 sb 2',不然会出错

// 超额输入
int a,b;
scanf("%d %d",&a,&b) // 输入的时候,必须是'1 2 3',不会出错,只会使用之前的内容

// 连续多输入
int a,b;
int c,d;
scanf("%d %d",&a,&b);
printf("%d sb, %d\n",a,b);
scanf("%d %d",&c,&d);
printf("%d sb, %d",c,d);
// 输入的时候,可以是 1 2 3 4 ,也能正确输出 <-- 这是因为缓存区的存在

继续读取或读取失败

int main(){
    int a,b;
    int c,d;
    scanf("%d %d",&a,&b);
    printf("%d sb, %d\n",a,b);
    scanf("%d %d",&c,&d);
    printf("%d sb, %d",c,d);
	
	return 0;
}
// 输入 1 2 回车
// prinf()... 然后等待输入
// 输入 3 4 回车
// printf()... 结束

//输入 1 2 a10 (第三个是一个不符合要求的数据)
// 直接结束,上下的 c,d 显示的系统默认的初始值

int a=1;
int b=2;
scanf("%d %d",&a,&b);
printf("%d sb, %d\n",a,b);
// 输入 1 a10
// 不会出错,b输出的是初始值(和上面的例子一样)

scanf / %c / %s

https://blog.csdn.net/github_38996275/article/details/73024270
https://blog.csdn.net/weixin_53416771/article/details/128379399

https://ask.csdn.net/questions/7655827
https://www.cnblogs.com/windpiaoxue/p/9184194.html

scanf 字符串

字符串类型,有两种定义方式,对应在内存中有两种 读取与写入权限
在读取时,要求字符串有写入权限,因此只能用char s[]="string"这种形式
scanf在读取字符串的时候,在输入时,以空格当做输入结束标志
实际上,scanf 遇到空白字符就停止读取,\t \n 空格

char s[10];

scanf("%s",s) // 这里不需要取地址符& // 不能加\n
scanf("%9s",s); // 不是默认设定的,而是严格指定读取的字符数,无论如何溢出都只读取指定的字符数

// 下面是错误的
// char *s2;
// scanf("%s",s)

puts/gets

puts

puts输出字符串,自动换行

char *str="hello world";

puts(&str[6]);
puts(str+6);

gets

gets读取正常,则返回指向字符串的地址
读取错误或遇到文件结尾,返回NULL空指针
gets丢掉输入里的换行符\n

gets接收字符串,与scanf的区别见下

scanf空格作为结束的标志,gets只把换行符作为字符串结束的标志

char a[10]={0};
char *ptr;
ptr=gets(a); // gets返回一个char*指针
printf("%s\n",a);
printf("%d\n",sizeof(a)); //10
while(gets(name)!=NULL) {}

// 等价于
char ch;
while((ch=getchar())!=EOF) {} // 判断是否读到文件末尾

注意

不检查预留存储区是否能够容纳实际输入的数据,多出来的字符会简单的溢出到相邻的内存区,从而造成错误

char a[10]={0};
gets(a); // > helloworldcmd 并不会报错

sprintf

把格式化字符串输出到指定字符串

#include <stdio.h>
// sprintf(char*buffer,const char* format,[arg]) // 返回buffer指向的字符串的长度
// buffer 指向要被写入的字符串指针
// format 格式化字符串
// argment 任意类型数据
int a=123;
double b=456.7;
char s[10];
sprintf(s,"%d;%f",a,b);
printf("%s\n",s);

5.结构体

结构体本质上是一种数据类型
结构体中的变量或数组,叫做结构体的成员

结构体可以包含其他结构体

结构体也是一维数组,用.获取单个成员

结构体是一种自定义的数据类型,是创建变量的模板,声明不占用内存空间,结构体变量则占用内存空间
结构体!=结构体变量:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,如int,float,char这些关键字本身不占用内存,结构体变量才是实实在在的数据,才需要内存来存储

5.1 创建

结构体是一种数据类型,可以用来定义变量

c语言在定义结构体的时候必须使用struct
c++语言在定义结构体的时候可以省略struct
推荐将结构体定义为全局变量

声明

结构体的声明,并没有创建一个实际的数据对象 ,只是告诉编译器如何表示数据,但是没有让计算机为数据分配空间 ;创建结构体变量后,才会分配实际的空间

struct STU
{
	char *name;
	int age;
}

创建

struct stu{
	char* name;
	int num;
}; // 注意分号

// 定义变量
struct stu stu1,stu2; // 必须加上关键字

// 在定义结构体的同时可以定义结构体变量
struct str{
	char* name;
	int num;
} stu1,stu2;

// 定义变量
struct stu stu_var={"li",20};

// 在结构体外,对成员进行赋值
stu1.name="tom";
stu1.num=12;

struct str{
	char* name;
	int num;
} stu1={"tom",1};

// 无结构体名,后面不能再次使用

注意

结构体内的数组与指针

struct Name
{
    char name[10];
};

int main()
{
    struct Name person={"li"};
    cout<<person.name<<endl;

    // 下面这种是不行的 相当于已经分配了字符数组的空间,而用一个字符常量的地址进行赋值
    // 是因为有可能指针在分配空间的时候指向了一个不可修改的内存空间
    // struct Name person2;
    // person2.name="wang";
    // cout<<person.name<<endl;

    return 0;
}

改进

#include <iostream>
#include <cstring>
using namespace std;

void getinfo(struct names *pst);
void makeinfo(struct names *pst);
void showinfo(const struct names *pst);
void cleanup(const struct names *pst); // 注意 虽然是释放,但是可以是const

struct names
{
    char *fname;
    char *lname;
    int letters;
};

int main()
{
    struct names person; 
    getinfo(&person);
    makeinfo(&person);
    showinfo(&person);
    
    // 释放内存
    cleanup(&person);

    return 0;
}

void getinfo(struct names *pst)
{
    cout<<"input fname lname"<<endl;
    
    char temp[10];
    gets(temp);
    pst->fname= (char *)malloc(strlen(temp)+1); // strlen实际的字符个数,不加\0
    strcpy(pst->fname,temp); 

    gets(temp);
    pst->lname= (char *)malloc(strlen(temp)+1); // strlen实际的字符个数,不加\0
    strcpy(pst->lname,temp); 
}

void makeinfo(struct names *pst)
{
    pst->letters=strlen(pst->fname)+strlen(pst->lname);
}

void showinfo(const struct names *pst)
{
    printf("%s,%s,%d\n",pst->fname,pst->lname,pst->letters);
}

void cleanup(const struct names *pst)
{
    free(pst->fname);
    free(pst->lname);
}

结构体之间可以进行赋值操作
数组之间不可以进行赋值操作

struct1=struct2

结构体嵌套

类能嵌套,结构体就能嵌套
注意自引用:https://blog.csdn.net/quanydi/article/details/126911953

struct LNode
{
    int data;
    struct LNode *next; // 用于指向下一个节点
};
struct names
{
    char first[LEN];
    char last[LEN];
};

struct people
{
    struct names handle;
    char favfool[LEN];
    char job[LEN];
    float income;
};

int main()
{
    struct people people1={
        {"li","wang"},
        "apple",
        "ex",
        20.1
    };

    printf("%s\n",people1.handle.first); 
    printf("%s\n",people1.handle.last); 

	return 0;
}

5.2 结构体的内存对齐

CPU 通过地址总线访问内存,一次处理4或8个字节,步长4或8
一个变量最好位于寻址步长的范围内,这样一次就可以读取到变量的值
内存对齐:将数据尽量放在一个步长之内,避免跨步存储,就称为内存对齐
默认4字节对齐(32位系统),8字节对齐(64位系统)

对齐规则

第一个成员在于结构体偏移量0的地址处
其他成员变量:取minimize(sizeof(dataType,systemByte)),即取数据类型所占字节数与系统默认对齐数的最小值,作为该数据的对齐数;该成员要对齐到(计算出的最小对齐数)的整数倍的地址处
结构体总大小为所有数据类中的最大对齐数的整数倍,且能最小包含所有数据的字节占位
注意:浪费的字节数不能大于系统默认的对齐字节数

// 注意顺序
struct data
{
    char b;		// 1/8 取1 占0位 
    int a;		// 4/8 取4 占4-7位
    short c;	// 2/8 取2 占8-9位
};
cout<<sizeof(data)<<endl; //12=4*3
struct data
{
    int a; 		// 4/8 取4 占0-3位
    char b;		// 1/8 取1 占4位 
    short c;	// 2/8 取2 占6,7位
};
cout<<sizeof(data)<<endl; //8=4*2 

修改对齐方式
https://blog.csdn.net/weixin_46251230/article/details/123755070

#pragma pack(1) // 设置默认为1个字节对齐
struct s{};
#pragma pack() // 取消设置的默认对齐数,换源默认值

进阶

不同的系统,对齐的规则略有不同

/* linux */
// 声明结构体
struct msgBuffer
{
    long mtype; // 4/8 4  0-3
    char mtext[100]; // 1/8  1 100/8=12*8=96 100-96=4 8-107!!! 
    int num; // 4/8 4 107-111
}; 
cout<<sizeof(msgBuffer)<<endl; // 112

/* windows */
struct msgBuffer
{
    long mtype; // 4/8 4  0-3
    char mtext[100]; // 1/8  1 100/8=12*8=96 100-96=4 4-103!!! 
    int num; // 4/8 4 104-107
}; 
cout<<sizeof(msgBuffer)<<endl; // 108

5.3 结构体数组/指针

5.3.1 结构体数组

struct stu{
	char* name;
	int num;
}class[5];

// 声明的时候初始化
struct stu{
	char* name;
	int num;
}class[5]={
	{"li",1},
	{"w",2}
};

// 声明的时候初始化,自动推断元素个数
struct stu{
	char* name;
	int num;
}class[]={
	{"li",1},
	{"w",2}
};

// 可以在结构体外,创建结构体数组
struct stu class[]={
    {"li",2},
    {"wang",3}
};

// 访问数据
class[0].name;
class[1].num=2; //修改

5.3.2 结构体指针

struct str{
	char* name;
	int num;
} stu1={"tom",1};

struct stu* pstu=&stu1;

// 直接创建指针
struct str{
	char* name;
	int num;
} stu1={"tom",1},*pstu=&stu1;

// 使用指针获取结构体成员
(*pstu).name; // 必须加括号
pstu->name; // 直接通过结构体指针获取成员,这种方法更有效

结构体指针的强制类型转换

https://blog.csdn.net/u011436427/article/details/122808350!!!
相当于内存赋值

5.3.3 结构体数组/指针 进阶

结构体名 不是该结构体的起始地址,更不是该结构体首元素的地址
结构体的起始地址,在值上 等于 该结构体首元素的地址

#include <iostream>

#define LEN 10

using namespace std;

struct names
{
    char first[LEN];
    char last[LEN];
};

struct people
{
    struct names handle;
    char favfool[LEN];
    char job[LEN];
    float income;
};


int main()
{
    // 结构体名 不是该结构体的起始地址
    // 结构体的起始地址,在值上 等于 该结构体首元素的地址
    struct names name1={"li","wang"};
    // std::cout<<name1<<endl;
    printf("%p\n",name1); // 0x61fd40 不等于首元素的地址
    printf("%p\n",&name1); // 0x61fdf0 相当于整个结构体的起始地址 而结构体的起始地址 的值 等于第一个成员的地址
    printf("%p\n",name1.first); // 0x61fdf0 
    printf("%p\n",&name1.first); // 0x61fdf0 该成员(整个成员)的起始地址
    printf("%p\n",&name1.first[0]); // 0x61fdf0 等价于 name1.first

    struct people people1={
        {"li","wang"},
        "apple",
        "ex",
        20.1
    };

    printf("%s\n",people1.handle.first); 
    printf("%s\n",people1.handle.last); 
    printf("%p\n",people1); // 0x61fd10 !!! 说明结构体名不等于首元素的地址也不等于结构体的起始地址
    printf("%p\n",&people1); // 0x61fdc0 整个外层结构体的起始地址
    printf("%p\n",people1.handle); // 0x61fd10 !!! 说明结构体名不等于首元素的地址也不等于结构体的起始地址
    printf("%p\n",&people1.handle); // 0x61fdc0 整个内存结构体的起始地址 等于 整个外层结构体的起始地址
    printf("%p\n",people1.handle.first); // 0x61fdc0 下面三个是数组的地址
    printf("%p\n",&people1.handle.first); // 0x61fdc0 
    printf("%p\n",&people1.handle.first[0]); // 0x61fdc0 
  
    struct people peoples[2]={
        {
            {"li","wang"},
            "apple",
            "ex",
            20.1
        },
        {
            {"zhang","san"},
            "pea",
            "ex2",
            22.1
        },
    };

    // 结构体指针
    struct people *ptr1;
    ptr1=&people1; // 结构体的名字并不等于结构体元素的首地址!!! 指向起始地址是对的,因为类型是匹配的
    cout<<ptr1->favfool<<","<<ptr1->job<<","<<ptr1->income<<endl;

    // 结构体数组指针 把他想象成普通数组的指针就行了
    struct people * ptr;
    // ac - 84 = int 44 一共44个字节
    cout<<&peoples[0]<<" "<<&peoples[1]<<endl; // 0x61fd80 0x61fdac 这个相等于起始地址
    cout<<&peoples<<endl; // 0x61fd80 整个数组的地址
    cout<<peoples<<endl; // 数组首元素的地址,也就是第一个结构体的地址
    // 结构体的名字并不等于结构体元素的首地址!!! 
    
    // version1
    ptr=peoples;
    cout<<ptr<<" "<<ptr+1<<endl; // 0x61fd80 0x61fdac
    cout<<ptr->job<<endl;
    
    // version2
    ptr=&peoples[0]; // struct people 所以+1 跨过了整个数组
    cout<<ptr<<" "<<ptr+1<<endl; // 0x61fd80 0x61fdac ptr+1跨过了整个结构体数组中的一个元素
    cout<<ptr->income<<endl;


    return 0;
}

6. 枚举类型

https://blog.csdn.net/weixin_45380951/article/details/105483270
https://blog.csdn.net/qq_53357769/article/details/111119060

整型常量

enum typename{var1,var2,...}
枚举值默认从0开始,后面逐渐+1
枚举列表中的标识符是常量,不能对再进行赋值,只能将varx赋值给其他变量!!!

可以把枚举类型 类比成 宏定义 #define name var
与宏定义不同的是:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值
varx不占用数据区(c)

enum week{Mon,Tues}; // 默认从1开始
enum week{Mon=1,Tues}; // 部分赋值,从1开始
enum week{Mon=1,Tues=2}; // 全部赋值

定义变量

// version1
enum week{Mon=1,Tues=2};
enum week a,b,c;

// version2 
enum week{Mon=1,Tues=2} a,b,c;

// 对定义的变量进行赋值
enum week{Mon=1,Tues=2};
enum week a=Mon,b=Tues;

enum week{Mon=1,Tues=2} a=Mon,b=Tues;

例子1

int a=2;
enum nums{one,tow,three}; // 可以把它当做类型
// 定义变量
enum nums num; // 变量要么是one\two\three

for (num=one;num<=three;)
{
    if (num==a)
    {
        cout<<"done"<<endl;
    }
    num=(enum nums)(num+1); // c语言枚举类型可以++ 而c++ enum类型不可以++ 只能用这种方式!!!
}

6.1 enum/int

只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量

enum nums{one,two,three,four,five};
enum nums num;

num=one; // 这是可以的

num=1; // 这种是错误的

// 只能强制类型转换 其意义是将顺序号为 2 的枚举元素赋予枚举变量num !!!
num=(enum nums)2; //等价于num=three ,顺序从0开始

下面例子也很重要

enum Day{Mo=1,To};

int a=0;
Day day=Mo;
cout<<(day==1)<<endl; // 1
cout<<(day==0)<<endl; // 0
cout<<(day==a)<<endl; // 0 

cout<<"======="<<endl;
Day day2=(enum Day)1; 
cout<<day2<<endl;       // 1
cout<<(day2==1)<<endl;  // 1 
cout<<(day2==0)<<endl;  // 0
cout<<(day2==Mo)<<endl; // 1

6.2 enum/swith-case

enum nums{one,two,three,four,five};
enum nums num;
int a=3;
num=(enum nums)a; // 顺序3 从0开始,输出four!!!

switch (num)
{
case one:
    cout<<"one"<<endl;
    break;
case two:
    cout<<"two"<<endl;
    break;
case three:
    cout<<"three"<<endl;
    break;
case four:
    cout<<"four"<<endl; 
    break;
case five:
    cout<<"five"<<endl;
    break;
default:
    break;
}
#include <stdio.h>
 
int main(){
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day; // day是整型常量
    scanf("%d", &day);
    switch(day){
        case Mon: puts("Monday"); break;
        case Tues: puts("Tuesday"); break;
        case Wed: puts("Wednesday"); break;
        case Thurs: puts("Thursday"); break;
        case Fri: puts("Friday"); break;
        case Sat: puts("Saturday"); break;
        case Sun: puts("Sunday"); break;
        default: puts("Error!");
    }
    return 0;
}

6.3 重定义问题

下面代码是不对的
问题在于两个枚举类型都使用相同的标识符"Off"和"On"来表示不同的值,这会导致编译时的命名冲突错误
两个枚举类型都定义了相同的标识符"Off"和"On"。这意味着当你在代码中引用这些标识符时,编译器无法区分它们属于哪个枚举类型,从而导致命名冲突。
枚举类型可以被看作一组具有唯一标识符的常量值集合。每个枚举成员都是一个常量,它们的名称必须唯一。在你的代码中,两个枚举中的标识符"Off"和"On"是相同的,这违反了唯一性的原则

typedef enum{
	Inactive,
	Off,
	On
}A;

typedef enum{
	Off,
	On
}B;

6.4 其他问题

枚举类型名可以和变量名相同

#include <iostream>

using namespace std;

enum DAY{mo,du};

int main(int argc, char const *argv[])
{
    DAY DAY;
    DAY=mo;
    cout<<DAY<<endl;	// 0
    cout<<(DAY==0)<<endl;	// 1
    return 0;
}

7. 共用体(联合体)

共用体与结构体的区别

结构体各个成员会占用不同的内存,互相之间没有影响内存大于等于所有成员占用的内存总和 – 字节对齐的问题
共用体所有成员占用同一段内存,修改一个成员会影响其余所有成员内存覆盖技术,等于最长的成员占用的内存,同一时刻只保存成一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉

union data{
    int n;
    char ch;
    double f;
}
union data a,b,c;

union data{
    int n;
    char ch;
    double f;
}a,b,c;

// 也可以没有名称,就是之后不再被调用

7.1 联合体数组

union data{};

// 创建联合体数组
union data au[10]; // au是个数组 每个元素都是一个联合体

7.2 联合体指针

union data{};

union data *a; 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值