C++基础知识(二)之数据类型、指针和内存、数组

news2025/2/11 9:22:31

六、C++数据类型

1、sizeof运算符

sizeof运算符用于求数据类型或变量占用的内存空间。

用于数据类型:sizeof(数据类型)

用于变量:sizeof(变量名) 或 sizeof 变量名

注意:

在32位和64位操作系统中,同一种数据类型占用的内存空间可能不一样。

字符串(string)不是C++的基本数据类型,用sizeof求它占用内存的大小没有意义。

2、整型的基本概念

C++用int关键字来声明整型变量(int 是 integer 的简写)。

在声明整型变量的时候,可以在int关键字之前加signed、unsigned、short和long四种修饰符。

signed:有符号的,可以表示正数和负数。

unsigned:无符号的,只能表示正数,例如超女的身高和体重等。

short:短的,取值范围小,占用内存少。

long:长的,取值范围大,占用内存多。

类型简写类型全称长度取值范围
shortsigned short int2字节-32768~32767
unsigned shortunsigned short int2字节0~65535
intsigned int4字节-2147483648~2147483647
unsignedunsigned int4字节0~4294967295
longsigned long int8字节-9223372036854775808~9223372036854775807
unsigned longunsigned long int8字节0~18446744073709551615

注意:

整数的取值范围与计算机操作系统和C++语言编译器有关,没有一个固定的数值,我们可以根据它占用的内存大小来推断它的取值范围。

一个位的取值是 0 1 1=2^1-1 两个位的取值是 00 01 10 11 3=2^2-1 三个位的取值是 000 001 …… 111 7=2^3-1

a)一个字节有8个位,表示的数据的取值范围是2^8-1,即255。

b)如果占用的内存是两个字节,无符号型取值范围是2^8ⅹ2^8-1。

c)如果占用的内存是四个字节,无符号型取值范围是2^8ⅹ2^8ⅹ2^8ⅹ2^8-1。

d)如果占用的内存是八个字节,无符号型取值范围是2^8ⅹ2^8ⅹ2^8ⅹ2^8ⅹ2^8ⅹ2^8ⅹ2^8ⅹ2^8-1。

e)如果是有符号,取值范围减半,因为符号占一个位。

f)计算机用最高位1位来表达符号(0-正数,1-负数),unsigned修饰过的正整数不需要符号位,在表达正整数的时候比signed修饰的正整数取值大一倍。

给整型变量赋值不能超出它的取值范围,否则能产生不可预后的后果。

在实际开发中,为了防止超出取值范围,应该保证有足够的空间。

3、整数的书写

整数默认是十进制,一个表示十进制的数字不需要任何特殊的格式。

1)二进制

二进制由 0 和 1 两个数字组成,书写时必须以0b或0B(不区分大小写)开头。

以下是合法的二进制:

 int a = 0b101;    // 换算成十进制为 5
 int b = -0b110010;  // 换算成十进制为 -50
 int c = 0B100001;  // 换算成十进制为 33

以下是非法的二进制:

int m = 101010;  // 无前缀 0B,相当于十进制
int n = 0B410;   // 4不是有效的二进制数字

注意,C++标准并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字。换句话说,并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关系。

2)八进制

八进制由 0~7 八个数字组成,书写时必须以0开头(注意是数字 0,不是字母 o)。

以下是合法的八进制数:

int a = 015;    // 换算成十进制为 13
int b = -0101;   // 换算成十进制为 -65
int c = 0177777;  // 换算成十进制为 65535

以下是非法的八进制:

int m = 256;  // 无前缀 0,相当于十进制
int n = 03A2;  // A不是有效的八进制数字

3)十六进制

十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,书写时必须以0x或0X(不区分大小写)开头。

以下是合法的十六进制:

 int a = 0X2A;  // 换算成十进制为 42
 int b = -0XA0;  // 换算成十进制为 -160
 int c = 0xffff;  // 换算成十进制为 65535

以下是非法的十六进制:

 int m = 5A;   // 没有前缀 0X,是一个无效数字
 int n = 0X3H;  // H不是有效的十六进制数字

4)需要注意的坑

在C++中,不要在十进制数前面加0,会被编译器当成八进制。

还有,不要随便删掉别人程序中整数前面的0,它不是多余的。

4、C++11的long long类型

在VS中,long是4字节,32位。 -2147483648~2147483647

在Linux中,long是8字节,64位。 -9223372036854775808~9223372036854775807

C++11标准增了long long类型的整数,至少64位,且至少与long一样长。

在VS中,long long是8字节,64位。 -9223372036854775808~9223372036854775807

在Linux中,long和long long类型都是8字节,64位。

5、浮点型(实数型)

C++浮点型分三种:float(单精度)、double(双精度)、long double(扩展精度)。

三者的区别是表示有效数字的范围不同。

数据类型占用空间有效数字范围
float4字节7位有效数字 8位
double8字节15~16位有效数字 17位
long double不少于double不低于double 17位

注意:

  • 在VS和Linux中,long double占用的内存空间分别是8和16字节。

  • 有效数字包括了小数点前面和后面的数字。

  • C++缺省显示6位有效数字,如果要显示更多的有效数字,可以用printf()函数。

  • 浮点数的存储方法和整数不一样,比较复杂,如无必要,不用研究。

  • 在实际开发中,用整数代替浮点数,整数的运算更快,精度更高。9223372036854775807

浮点数的小数位为何很多

  1. 表示方式:在计算机中,浮点数是以IEEE 754标准表示的,包括符号位、指数位和尾数位。由于尾数位使用有限位数来表示,当将float类型转换为double类型时,由于double类型的精度更高,会在低位添加一些额外的精度,导致小数位增多。例如,在Java中,float类型占用32位,可以表示大约6到7位有效数字;而double类型占用64位,可以表示大约15到16位有效数字。因此,将float转换为double时,会出现小数位增多的情况。

  2. 精度扩展:在将低精度浮点数转换为高精度浮点数时,由于高精度类型能够表示更多的有效数字,因此会在转换后的数值中添加额外的精度,这些额外的精度通常以小数位的形式出现。

浮点数为何不精确

  1. 二进制表示限制:浮点数在计算机中以二进制形式表示,但许多十进制数无法完全精确地转换为二进制数。例如,0.1在二进制表示中是一个无限循环的小数,无法准确表示。这种表示方式的限制导致了浮点数的精度损失。

  2. 舍入误差:在浮点数运算中,由于计算机使用有限的位数来表示浮点数,因此计算结果可能需要舍入到最接近的可表示浮点数。这个舍入过程会引入舍入误差,从而导致结果不精确。

  3. 运算顺序的影响:浮点数运算的顺序会影响最终的结果。由于浮点数运算是一个逐步逼近的过程,运算顺序的不同可能导致结果的不同。这种运算顺序的不确定性也是浮点数不精确的原因之一。

  4. 机器精度限制:计算机使用有限的位数来表示浮点数,这就限制了浮点精度的范围。无论是单精度浮点数(float)还是双精度浮点数(double),都受到这种机器精度限制的影响。

综上所述,浮点数的小数位很多主要是由于其表示方式和精度扩展所导致的;而浮点数不精确则是由于二进制表示限制、舍入误差、运算顺序的影响以及机器精度限制等多种因素共同作用的结果。

6、字符型的基本概念

字符型(char)占用的内存空间是1个字节,书写用单引号包含。

在内存中,不存放字符本身,而是存放与它对应的编码,即ASCII码。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是现今最通用的单字节编码方案,包含了33个控制字符(具有特殊含义无法显示的字符)和95个可显示字符。

'X' -> 88 01011000 'a'->97 01100001 '3'->51 00110011

1)ASCII 控制字符(0~31)

十进制符号中文解释十进制符号中文解释
0NULL空字符16DLE数据链路转义
1SOH标题开始17DC1设备控制 1
2STX正文开始18DC2设备控制 2
3ETX正文结束19DC3设备控制 3
4EOT传输结束20DC4设备控制 4
5ENQ询问21NAK拒绝接收
6ACK收到通知22SYN同步空闲
7BEL23ETB传输块结束
8BS退格24CAN取消
9HT水平制表符25EM介质中断
10LF换行键26SUB替换
11VT垂直制表符27ESC换码符
12FF换页键28FS文件分隔符
13CR回车键29GS组分隔符
14SO移出30RS记录分离符
15SI移入31US单元分隔符

2)ASCII可显示字符(32~127)

十进制符号中文解释十进制符号中文解释
32空格80P大写字母 P
33!感叹号81Q大写字母 Q
34"双引号82R大写字母 R
35#井号83S大写字母 S
36$美元符84T大写字母 T
37%百分号85U大写字母 U
38&86V大写字母 V
39'单引号87W大写字母 W
40(左括号88X大写字母 X
41)右括号89Y大写字母 Y
42*星号90Z大写字母 Z
43+加号91[左中括号
44,逗号92\斜线
45-减号93]右中括号
46.句点或小数点94^音调符号
47/反斜线95_下划线
480数字0的符号96`重音符
491数字1的符号97a小写字母 a
502数字2的符号98b小写字母 b
513数字3的符号99c小写字母 c
524数字4的符号100d小写字母 d
535数字5的符号101e小写字母 e
546数字6的符号102f小写字母 f
557数字7的符号103g小写字母 g
568数字8的符号104h小写字母 h
579数字9的符号105i小写字母 i
58:冒号106j小写字母 j
59;分号107k小写字母 k
60<小于108l小写字母 l
61=等号109m小写字母 m
62>大于110n小写字母 n
63?问号111o小写字母 o
64@电子邮件符号112p小写字母 p
65A大写字母 A113q小写字母 q
66B大写字母 B114r小写字母 r
67C大写字母 C115s小写字母 s
68D大写字母 D116t小写字母 t
69E大写字母 E117u小写字母 u
70F大写字母 F118v小写字母 v
71G大写字母 G119w小写字母 w
72H大写字母 H120x小写字母 x
73I大写字母 I121y小写字母 y
74J大写字母 J122z小写字母 z
75K大写字母 K123{ 左大括号
76L大写字母 L124|竖线
77M大写字母 M125}右大括号
78N大写字母 N126~波浪号
79O大写字母 O127删除

a)32是空格。

b)48~57是0到9十个阿拉伯数字;

c)65~90是26个大写英文字母;

d)97~122号是26个小写英文字母;

e)其余的是一些标点符号、运算符号等;

f)第127个字符表示的是键盘上的删除键。

7、字符的本质

a)字符的本质是整数,取值范围是0~127。

b)在书写的时候可以用单引号包含,也可以用整数。

c)如果书写的时候用单引号包含,程序执行的时候,将把符号解释为对应的整数。

d)显示的时候,把整数解释为对应的符号,也可以直接显示整数。

d)可以与整数进行任何运算,运算的时候,书写方式可以用字符,也可以用整数。

8、转义字符

在C++程序中,使用转义字符的原因有两个:

控制字符没有符号,无法书写,只能用其它的符号代替。

某些符号已被C++征用,语义冲突,只能用其它的符号代替。

ASCII码值转义字符含义
0\0空,给字符型变量赋值时可以直接书写0。
10\n换行(LF) ,将当前位置移到下一行开头。
13\r回车(CR) ,将当前位置移到本行开头
9\t水平制表(HT) (跳到下一个TAB位置)
92*\*斜线
34"双引号,书写字符时不必转义。
39'单引号,书写字符串中不必转义。
7\a警报
8\b退格(BS) ,将当前位置移到前一列
12\f换页(FF),将当前位置移到下页开头
11\v垂直制表(VT)

9、C++11的原始字面量

原始字面量(值)可以直接表示字符串的实际含义,不需要转义和连接。

语法:R"(字符串的内容)"

R"xxx(字符串的内容)xxx"

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
    // 使用转义的方法
    string path = "C:\\Program Files\\Microsoft OneDrive\\tail\\nation";
    cout << "path is " << path << endl;

    // 使用C++11原始字面量
    string path1 = R"abcd(C:\Program Files\Microsoft OneDrive\tail\nation)abcd";
    cout << "path1 is " << path1 << endl;

    string str = R"(
        <no>0001</no>
        <name>西施</name>
        <sc>火树银花</sc>
        <yz>沉鱼</yz>
        <age>23</age>
        <weight>48.5</weight>
        <height>170</height>)";
    cout << str << endl;
}

10、字符串型

C++风格字符串:string 变量名="字符串的内容" ;

C风格字符串:char 变量名[]="字符串的内容" ;

C风格字符串的本质是字符数组,C++风格字符串的本质是类,它封装了C风格字符串。

C++风格字符串的常用操作:

赋值:变量名="字符串的内容" ;

拼接:变量名=变量名+"字符串的内容一"+"字符串的内容一"+......+"字符串的内容n" ;

如果字符串的内容都是常量,不要写加号(+),如果内容很长,可以分成多行书写。

比较:支持==、!=、>和<关系运算符,常用的是==和!=。

11、布尔型

在C和C++中,关系运算和逻辑运算的结果有两种:真和假。

C语言用0表示假,非0表示真。

为了提高代码的可读性,C++新增了 bool 类型,占用1字节的内存,用true表示真,false表示假。

bool类型本质上是1字节的整数(unsigned char),取值只有1和0。

在程序中,书写的时候可以用true和false,编译器把它们解释为1和0。

如果对bool型变量赋非0的值,将转换成1。

用cin输入和cout输出的时候,仍是1和0,不会被解释为true和false。

12、数据类型的转换

计算机进行运算时,要求各操作数的类型具有相同的大小和存储方式。

在实际开发中,不同类型的数据进行混合运算是基本需求。

自动类型转换:某些类型的转换编译器可以隐式的进行,不需程序员干预。

强制类型转换:有些类型的转换需要程序员显式指定。

1)自动类型转换

不同数据类型的差别在于取值范围和精度,数据的取值范围越大,精度越高。

整型从低到高:

char -> short -> int -> long -> long long

浮点型从低到高:

float -> double -> long double

自动类型转换的规则如下:

  • 如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换

  • 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。

  • 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型

  • 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。

2)强制类型转换

为了让程序设计更灵活,转换的目的更清晰,C++提供了强制类型转换的方法,也称之为显式转换。

强制类型转换的语法:(目标类型)表达式或目标类型(表达式)

注意:

  • 如果使用强制转换,表示程序员已有明确的目的。

  • 如果转换的行为不符合理,后果由程序员承担。

  • 如果采用了强制类型转换,编译的告警信息将不再出现。

  • 类型转换运算符的优先级比较高,如果没把握就加括号。

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	char a = 30;
	int    b = 102400;
	long long c = 15000000000001;

	// 如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换。
	cout << "a+b+c=" << a + b + c << endl;

	// 当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。
	cout << "8/5=" << ((double)8) / 5 << endl;

	// 赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型。
	// 赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。
	int d = (int)23.59;        // 降低了精度。
	cout << "d=" << d << endl;

	unsigned int e = (unsigned int)4294967295+10;       // 值被截断,从高位截断
	cout << "e=" << e << endl;
	// 4294967295         11111111111111111111111111111111
	// 4294967296     000100000000000000000000000000000000
	// 4294967297     000100000000000000000000000000000001
}

13、数据类型的别名typedef

创建数据类型的别名有两个目的:

为名称复杂的类型创建别名,方便书写和记忆。

创建与平台无关的数据类型,提高程序的兼容性。

语法:typedef 原数据类型名 别名;

C++11还可以用using关键字创建数据类型的别名。

语法:using 别名=原数据类型名;

14、C风格字符串

string使用方便,能自动扩展,不用担心内存问题

string是C++的类,封装了C风格字符串

在某些场景中,C风格字符串更方便、更高效

C语言约定:如果字符型(char)数组的末尾包含了空字符\0(也就是0),那么该数组中的内容就是一个字符串。

因为字符串需要用0结尾,所以在声明字符数组的时候,要预留多一个字节用来存放0。

char name[21]; // 声明一个最多存放20个英文字符或十个中文的字符串。

1)初始化方法

char name[11];         //可以存放10个字符,没有初始化,里面是垃圾值。

char name[11] = "hello";     // 初始内容为hello,系统会自动添加0。

char name[]  = { "hello" };    // 初始内容为hello,系统会自动添加0,数组长度是6。

char name[11] = { "hello" };    // 初始内容为hello,系统会自动添加0。

char name[11]  { "hello" };    // 初始内容为hello,系统会自动添加0。C++11标准。

char name[11] = { 0 };      //把全部的元素初始化为0。

2)清空字符串

memset(name,0,sizeof(name));  // 把全部的元素置为0。

name[0]=0;    // 不规范,有隐患,不推荐。

3)字符串复制或赋值strcpy()

char *strcpy(char* dest, const char* src);

功 能: 将参数src字符串拷贝至参数dest所指的地址。

返回值: 返回参数dest的字符串起始地址。

复制完字符串后,会在dest后追加0。

如果参数dest所指的内存空间不够大,会导致数组的越界。

4)字符串复制或赋值strncpy()

char * strncpy(char* dest,const char* src, const size_t n);

功能:把src前n个字符的内容复制到dest中。

返回值:dest字符串起始地址。

如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。

如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0。

如果参数dest所指的内存空间不够大,会导致数组的越界。

5)获取字符串的长度strlen()

 size_t  strlen( const char*  str);

功能:计算字符串的有效长度,不包含0。

返回值:返回字符串的字符数。

strlen()函数计算的是字符串的实际长度,遇到0结束。

6)字符串拼接strcat()

char *strcat(char* dest,const char* src);

功能:将src字符串拼接到dest所指的字符串尾部。

返回值:返回dest字符串起始地址。

dest最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个0。

如果参数dest所指的内存空间不够大,会导致数组的越界。

 7)字符串拼接strncat()

char *strncat (char* dest,const char* src, const size_t n);

功能:将src字符串的前n个字符拼接到dest所指的字符串尾部。

返回值:返回dest字符串的起始地址。

如果n大于等于字符串src的长度,那么将src全部追加到dest的尾部,如果n小于字符串src的长度,只追加src的前n个字符。

strncat会将dest字符串最后的0覆盖掉,字符追加完成后,再追加0。

如果参数dest所指的内存空间不够大,会导致数组的越界。

8)字符串比较strcmp()和strncmp()

int strcmp(const char *str1, const char *str2 );

功能:比较str1和str2的大小。

返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;

int strncmp(const char *str1,const char *str2 ,const size_t n);

功能:比较str1和str2前n个字符的大小。

返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;

两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。

在实际开发中,程序员一般只关心字符串是否相等,不关心哪个字符串更大或更小。

9)查找字符strchr()和strrchr()

const char *strchr(const char *s, int c);

返回在字符串s中第一次出现c的位置,如果找不到,返回0。

const char *strrchr(const char *s, int c);

返回在字符串s中最后一次出现c的位置,如果找不到,返回0。

10)查找字符串strstr()

char *strstr(const char* str,const char* substr);

功能:检索子串在字符串中首次出现的位置。

返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。

11)用于string的表达式

可以把C风格的字符串用于包含了string类型的赋值拼接等表达式中。

12)注意事项

a)字符串的结尾标志是0,按照约定,在处理字符串的时候,会从起始位置开始搜索0,一直找下去,找到为止(不会判断数组是否越界)。

b)结尾标志0后面的都是垃圾内容。

c)字符串在每次使用前都要初始化,减少入坑的可能,是每次,不是第一次。

d)不要在子函数中对字符指针用sizeof运算,所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中。

e)在VS中,如果要使用C标准的字符串操作函数,要在源代码文件的最上面

#define _CRT_SECURE_NO_WARNINGS

七、指针和内存

1、指针的基本概念

1)变量的地址

变量是内存变量的简称,在C++中,每定义一个变量,系统就会给变量分配一块内存,内存是有地址的。

C++用运算符&获取变量在内存中的起始地址。

语法:&变量名

2)指针变量

指针变量简称指针,它是一种特殊的变量,专用于存放变量在内存中的起始地址

语法:数据类型 *变量名;

数据类型必须是合法的C++数据类型(int、char、double或其它自定义的数据类型)。

星号*与乘法中使用的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针。

3)对指针赋值

不管是整型、浮点型、字符型,还是其它的数据类型的变量,它的地址都是一个十六进制数。我们用整型指针存放整数型变量的地址;用字符型指针存放字符型变量的地址;用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。

语法:指针=&变量名;

注意

  • 对指针的赋值操作也通俗的被称为“指向某变量”,被指向的变量的数据类型称为“基类型”。

  • 如果指针的数据类型与基类型不符,编译会出现警告。但是,可以强制转换它们的类型。

4)指针占用的内存

指针也是变量,是变量就要占用内存空间。

64位的操作系统中,不管是什么类型的指针,占用的内存都是8字节

在C++中,指针是复合数据类型,复合数据类型是指基于其它类型而定义的数据类型,在程序中,int是整型类型,int是整型指针类型,int可以用于声明变量,可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int*当成一种数据类型就是了。

2、使用指针

声明指针变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。

指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)

*运算符被称为间接值或解除引用(解引用)运算符,将它用于指针,可以得到该地址的内存中存储的值,*也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。

变量和指向变量的指针就像同一枚硬币的两面。

程序在存储数据的时候,必须跟踪三种基本属性:

  • 数据存储在哪里;

  • 数据是什么类型;

  • 数据的值是多少。

用两种策略可以达到以上目的:

声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元。

声明一个指针变量,存储的值是地址,而不是值本身,程序直接访问该内存单元。

3、指针用于函数的参数

如果把函数的形参声明为指针,调用的时候把实参的地址传进去,形参中存放的是实参的地址,在函数中通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递或传地址

值传递:函数的形参是普通变量。

传地址的意义如下:

  • 可以在函数中修改实参的值。

  • 减少内存拷贝,提升性能。

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

// 调用函数的时候,调用者把数值赋给了函数的参数。
// 实参:调用者程序中书写的在函数名括号中的参数。
// 形参:函数的参数列表。
void func(int *no, string *str)    // 向超女表白的函数。 
{
	cout << "亲爱的" << *no << "号:" << *str << endl;
	*no = 8;
	*str = "我有一只小小鸟。";
}

// 写一个函数,从3名超女的身高数据中,选出最高的和最矮的。
void func1(int a, int b, int c, int* max, int* min)
{
	*max = a > b ? a : b;               // 取a和b中的大者。
	*min = a < b ? a : b;                // 取a和b中的小者。
	*max = *max > c ? *max : c;   // 取*max和c中的大者。
	*min = *min < c  ? *min : c;    // 取*min和c中的大者。
}

int main()
{
	int bh = 3;      // 超女的编号。
	string message = "我是一只傻傻鸟。";          // 向超女表白的内容。

	func(&bh, &message);            // 调用向超女表白的函数。
	/*{
		int *no = &bh;          
		string *str = &message; 

		cout << "亲爱的" << *no << "号:" << *str << endl;
		*no = 8;
		*str = "我有一只小小鸟。";
	}*/

	cout << "亲爱的" << bh << "号:" << message << endl;

	// 从3名超女的身高数据中,选出最高的和最矮的。
	int a = 180, b = 170, c = 175, m, n;
	func1(a, b, c, &m, &n);
	cout << "m=" << m << ",n=" << n << endl;
}

4、用const修饰指针

1)常量指针

语法:const 数据类型 *变量名;

不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。

int a = 8;
const *pa = &a;
a = 10;   //可以修改
*pa = 10;  //不可以修改,会报错

//指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
int b = 1;
pa = &b;  //可以

注意:

  • 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。

  • 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。

  • 如果用于形参,虽然指向的对象可以改变,但这么做没有任何意义。

  • 如果形参的值不需要改变,建议加上const修饰,程序可读性更好。

2)指针常量

语法:数据类型 * const 变量名;

指向的变量(对象)不可改变。

int* const c;  //不做初始化,会报错
int a = 10;
int b = 20;
int* const p = &a;
*p = 20;  //可以修改
p = &b;   //不可以,会报错

注意:

  • 在定义的同时必须初始化,否则没有意义。

  • 可以通过解引用的方法修改内存地址中的值。

  • C++编译器把指针常量做了一些特别的处理,改头换面之后,有一个新的名字,叫引用。

3)常指针常量

语法:const 数据类型 * const 变量名;

指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。(常引用)

常量指针:指针指向可以改,指针指向的值不可以更改。

指针常量:指针指向不可以改,指针指向的值可以更改。

常指针常量:指针指向不可以改,指针指向的值不可以更改。

记忆秘诀:*表示指针,指针在前先读指针;指针在前指针就不允许改变。

常量指针:const 数据类型 *变量名

指针常量:数据类型 * const 变量名

5、void关键字

在C++中,void表示为无类型,主要有三个用途:

1)函数的返回值用void,表示函数没有返回值。

void func(int a,int b)
{
  // 函数体代码。
  return;
}

2)函数的参数填void,表示函数不需要参数(或者让参数列表空着)。

int func(void)
{
  // 函数体代码。
  return 0;
}

3)函数的形参用void *,表示接受任意数据类型的指针。

#include <iostream>        
using namespace std; 

//varname-变量名,p-变量的地址。
// 只关心地址本身,不关心里面的内容,用void *可以存放任意类型的地址。
void func(string varname, void* p)
{
	cout << varname<< "的地址是:" << p << endl;
	cout << varname << "的值是:" << *(char *)p << endl;
}


int main()
{
	int a=9;
	char b='X';

	cout << "a的地址是:" << &a << endl;  //a的地址是:00000032D24FF6A4
	cout << "b的地址是:" << &b << endl;  //b的地址是:烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫??
	
	//显示变量的十六进制地址用  void *
	cout << "a的地址是:" << (void *) & a << endl;    //a的地址是:0000003F675FF6F4
	cout << "b的地址是:" << (void *) & b << endl;    //b的地址是:0000003F675FF714
	
	func("a", &a);
	func("b", &b);
}


注意:

  • 不能用void声明变量,它不能代表一个真实的变量,但是,用void *可以。

  • 不能对void *指针直接解引用(需要转换成其它类型的指针)。

  • 把其它类型的指针赋值给void*指针不需要转换。

  • 把void *指针赋值给把其它类型的指针需要转换。

6、C++内存模型

在 C++ 中,程序运行时,内存主要分成四个区,分别是栈、堆、数据段代码段

栈:存储局部变量、函数参数和返回值。

堆:存储动态开辟内存的变量。

数据段:存储全局变量和静态变量。

代码段:存储可执行程序的代码和常量(例如字符常量),此存储区不可修改。

栈和堆的主要区别: 1)管理方式不同:栈是系统自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程序中不释放,程序结束时由操作系统回收。 2)空间大小不同:堆内存的大小受限于物理内存空间;而栈就小得可怜,一般只有8M(可以修改系统参数)。

3)分配方式不同:堆是动态分配;栈有静态分配和动态分配(都是自动释放)。

4)分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由C++函数库提供的。

5)是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片;而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片,太多的碎片会导致性能的下降。

6)增长方向不同:栈向下增长,以降序分配内存地址;堆向上增长,以升序分配内存地址。

7、动态分配内存new和delete

使用堆区的内存有四个步骤:

1)声明一个指针;

2)用new运算符向系统申请一块内存,让指针指向这块内存;

3)通过对指针解引用的方法,像使用变量一样使用这块内存;

4)如果这块内存不用了,用delete运算符释放它。

申请内存的语法:new 数据类型(初始值); // C++11支持{}

如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)。

释放内存的语法:delete 地址;

释放内存不会失败。

注意:

  • 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。

  • 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。

  • 动态分配的内存生命周期与程序相同,程序退出时,如果没有释放,系统将自动回收。

  • 就算指针的作用域已失效,所指向的内存也不会释放。

  • 用指针跟踪已分配的内存时,不能跟丢。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	// 1)声明一个指针;
	// 2)用new运算符向系统申请一块内存,让指针指向这块内存;
	// 3)通过对指针解引用的方法,像使用变量一样使用这块内存;
	// 4)如果这块内存不用了,用delete运算符释放它。
	// 申请内存的语法:new 数据类型(初始值);   // C++11支持{}
	// 释放内存的语法:delete 地址;
	int* p = new int(5);
	cout << "*p=" << *p << endl;
	*p = 8;
	cout << "*p=" << *p << endl;
	delete p;

	/*	for (int ii = 1; ii > 0; ii++)
	{
		int* p = new int[100000];     // 一次申请100000个整数,这个语法以后再讲。
		cout << "ii="<<ii<<",p=" << p << endl;
	}/*
}

8、二级指针

指针是指针变量的简称,也是变量,是变量就有地址。

指针用于存放普通变量的地址。

二级指针用于存放指针变量的地址。

声明二级指针的语法:数据类型 ** 指针名;

使用指针有两个目的:1)传递地址;2)存放动态分配的内存的地址。

在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针。

把普通变量的地址传入函数后可以在函数中修改变量的值;把指针的地址传入函数后可以在函数中指针的值。

9、空指针

在C和C++中,用0或NULL都可以表示空指针。

声明指针后,在赋值之前,让它指向空,表示没有指向任何地址。

1)使用空指针的后果

如果对空指针解引用,程序会崩溃。

如果对空指针使用delete运算符,系统将忽略该操作,不会出现异常。所以,内存被释放后,也应该把指针指向空。

在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。

为什么空指针访问会出现异常?

NULL指针分配的分区:其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。

2)C++11的nullptr

用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是(void *)0。

NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整型的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议用nullptr替代NULL吧,而NULL就当做0使用。

注意:在Linux平台下,如果使用nullptr,编译需要加-std=c++11参数。

10、野指针

野指针就是指针指向的不是一个有效(合法)的地址。

在程序中,如果访问野指针,可能会造成程序的崩溃。

出现野指针的情况主要有三种:

1)指针在定义的时候,如果没有进行初始化,它的值是不确定的(乱指一气)。

2)如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地址已失效。

3)指针指向的变量已超越变量的作用域(变量的内存空间已被系统回收),让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回值赋给了指针。

规避方法:

1)指针在定义的时候,如果没地方指,就初始化为nullptr。

2)动态分配的内存被释放后,将其置为nullptr。

3)函数不要返回局部变量的地址。

注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序的崩溃。是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。

11、函数指针

函数的二进制代码存放在内存四区中的代码段,函数的地址是它在内存中的起始地址。如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其它函数。

使用函数指针的三个步骤:

a)声明函数指针;

b)让函数指针指向函数的地址;

c)通过函数指针调用函数。

1)声明函数指针

声明普通指针时,必须提供指针的类型。同样,声明函数指针时,也必须提供函数类型,函数的类型是指*返回值**参数列表*(函数名和形参名不是)

假设函数的原型是:

int func1(int bh,string str);

int func2(int no,string message);

int func3(int id,string info);

bool func4(int id,string info); 

bool func5(int id);

则函数指针的声明是:

int  (*pfa)(int,string);

bool (*pfb)(int,string);

bool (*pfc)(int);

pfa、pfb、pfc是函数指针名,必须用括号,否则就成了返回指针的函数。

2)函数指针的赋值

函数名就是函数的地址。

函数指针的赋值:函数指针名=函数名;

3)函数指针的调用

(*函数指针名)(实参);

函数指针名(实参);

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

void func(int no, string str)
{
	cout << "亲爱的" << no << "号:" << str << endl;
}

int main()
{
	int bh = 3;                                                 // 超女的编号。
	string message = "我是一只傻傻鸟。";    // 向超女表白的内容。

	func(bh, message);

	void (*pfunc)(int, string);           // 声明表白函数的函数指针。
	pfunc = func;                              // 对函数指针赋值,语法是函数指针名=函数名。
	pfunc(bh, message);                  // 用函数指针名调用函数。 C++
	(*pfunc)(bh, message);              // 用函数指针名调用函数。 C语言
}
#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

void zs(int a)         // 张三的个性化表白函数。
{
	cout  <<"a=" << a << "我要先翻三个跟斗再表白。\n";   // 个性化表白的代码。
}

void ls(int a)         // 李四的个性化表白函数。
{
	cout << "a=" << a << "我有一只小小鸟。\n";   // 个性化表白的代码。
}

void show(void (*pf)(int),int b)
{
	cout << "表白之前的准备工作已完成。\n";       // 表白之前的准备工作。
	pf(b);                                                                     // 用函数指针名调用个性化表白函数。
	cout << "表白之后的收尾工作已完成。\n";       // 表白之后的收尾工作。
}

int main()
{
	show(zs, 3);          // 张三要表白。
	show(ls, 4);          // 李四要表白。
}

八、数组

1、一维数组的基本概念

数组是一组数据类型相同的变量,可以存放一组数据。

1)创建数组

声明数组的语法:数据类型 数组名[数组长度];

注意:数组长度必须是整数,可以是常量,也可以是变量和表达式。

C90规定必须用常量表达式指明数组的大小,C99允许使用整型非常量表达式。经测试,在VS中可以用用整型非常量表达式,不能用变量;但是,Linux中还可以用变量。

2)数组的使用

可以通过下标访问数组中元素,数组下标从0开始。

数组中每个元素的特征和使用方法与单个变量完全相同。

语法:数组名[数组下标]

注意:

数组下标也必须是整数,可以是常量,也可以是变量。

合法的数组下标取值是:0~(数组长度-1)。

3)数组占用内存的情况

数组在内存中占用的空间是连续的。

用sizeof(数组名)可以得到整个数组占用内存空间的大小(只适用于C++基本数据类型)。

4)数组的初始化

声明的时候初始化:

数据类型 数组名[数组长度] = { 值1,值2,值3, ...... , 值n};

数据类型 数组名[ ] = { 值1,值2,值3, ...... , 值n};

数据类型 数组名[数组长度] = { 0 };  // 把全部的元素初始化为0。

数据类型 数组名[数组长度] = { };   // 把全部的元素初始化为0。

注意:如果{}内不足数组长度个数据,剩余数据用0补全,但是,不建议这么用,你可能在数组中漏了某个值。如果想把数组中全部的元素初始化为0,可以在{}内只填一个0或什么也不填。

C++11标准可以不写等于号。

5)清空数组

memset()函数可以把数组中全部的元素清零。(只适用于C++基本数据类型)

函数原型:void *memset(void *s, int c, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

6)复制数组

memcpy()函数可以把数组中全部的元素复制到另一个相同大小的数组。(只适用于C++基本数据类型)

函数原型:void *memcpy(void *dest, const void *src, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	int bh[] = {3, 6, 1,6,7,4,3,5,6,7,8,322,2,3,9};               // 超女编号。
	string name[3];    // 超女姓名。
	
	for (int ii = 0; ii < sizeof(bh)/sizeof(int); ii++)
	{
		cout << "bh["<<ii<<"]=" << bh[ii] << endl;
	}
	
	memset(bh,0,sizeof(bh));   //将数组bh清零

	int bh1[sizeof(bh) / sizeof(int)];   // 数组长度必须是整数,可以是常量,也可以是变量和表达式。

	memcpy(bh1, bh, sizeof(bh));      // 把数组bh中的内容复制到bh1。    

	for (int ii = 0; ii < sizeof(bh1) / sizeof(int); ii++)
	{
		cout << "bh1[" << ii << "]=" << bh1[ii] << endl;
	}
}

2、一维数组和指针

1)指针的算术

将一个整型变量加1后,其值将增加1。

但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	char a;      cout << "sizeof(char)=" << sizeof(char) << endl;            // 1字节
	short b;    cout << "sizeof(short)=" << sizeof(short) << endl;         // 2字节
	int c;         cout << "sizeof(int)=" << sizeof(int) << endl;                  // 4字节
	double d; cout << "sizeof(double)=" << sizeof(double) << endl;   // 8字节
		
	cout << "a的地址是:" << (void *)& a << endl;
	cout << "a的地址+1是:" << (void*)( & a + 1) << endl;

	cout << "b的地址是:" << (void*)&b << endl;
	cout << "b的地址+1是:" << (void*)(&b + 1) << endl;

	cout << "c的地址是:" << (void*)&c << endl;
	cout << "c的地址+1是:" << (void*)(&c + 1) << endl;

	cout << "d的地址是:" << (void*)&d << endl;
	cout << "d的地址+1是:" << (void*)(&d + 1) << endl;
}

输出:
sizeof(char)=1
sizeof(short)=2
sizeof(int)=4
sizeof(double)=8
a的地址是:0000003F34BDF774
a的地址+1是:0000003F34BDF775
b的地址是:0000003F34BDF794
b的地址+1是:0000003F34BDF796
c的地址是:0000003F34BDF7B4
c的地址+1是:0000003F34BDF7B8
d的地址是:0000003F34BDF7D8
d的地址+1是:0000003F34BDF7E0

2)数组的地址

a)数组在内存中占用的空间是连续的。

b)C++将数组名解释为数组第0个元素的地址。

c)数组第0个元素的地址和数组首地址的取值是相同的。

d)数组第n个元素的地址是:数组首地址+n

e)C++编译器把 数组名[下标] 解释为(数组首地址+下标)

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	double a[5];

	cout << "a的值是:" << (long long)a << endl;
	cout << "&a的值是:" << (long long)&a << endl;

	cout << "a[0]的地址是:" << (long long)&a[0] << endl;
	cout << "a[1]的地址是:" << (long long)&a[1] << endl;
	cout << "a[2]的地址是:" << (long long)&a[2] << endl;
	cout << "a[3]的地址是:" << (long long)&a[3] << endl;
	cout << "a[4]的地址是:" << (long long)&a[4] << endl;

	double* p = a;
	cout << "p的值是:" << (long long)p << endl;
	cout << "p+0的值是:" << (long long)(p + 0) << endl;
	cout << "p+1的值是:" << (long long)(p + 1) << endl;
	cout << "p+2的值是:" << (long long)(p + 2) << endl;
	cout << "p+3的值是:" << (long long)(p + 3) << endl;
	cout << "p+4的值是:" << (long long)(p + 4) << endl;
}

a的值是:429466843736
&a的值是:429466843736
a[0]的地址是:429466843736
a[1]的地址是:429466843744
a[2]的地址是:429466843752
a[3]的地址是:429466843760
a[4]的地址是:429466843768
p的值是:429466843736
p+0的值是:429466843736
p+1的值是:429466843744
p+2的值是:429466843752
p+3的值是:429466843760
p+4的值是:429466843768

3)数组的本质

数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	int a[5] = { 3 , 6 , 5 , 8 , 9 };

	// 用数组表示法操作数组。
	cout << "a[0]的值是:" << a[0] << endl;
	cout << "a[1]的值是:" << a[1] << endl;
	cout << "a[2]的值是:" << a[2] << endl;
	cout << "a[3]的值是:" << a[3] << endl;
	cout << "a[4]的值是:" << a[4] << endl;

	// 用指针表示法操作数组。
	int* p = a;
	cout << "*(p+0)的值是:" << *(p+  0) << endl;
	cout << "*(p+1)的值是:" << *(p + 1) << endl;
	cout << "*(p+2)的值是:" << *(p + 2) << endl;
	cout << "*(p+3)的值是:" << *(p + 3) << endl;
	cout << "*(p+4)的值是:" << *(p + 4) << endl;
}

a[0]的值是:3
a[1]的值是:6
a[2]的值是:5
a[3]的值是:8
a[4]的值是:9
*(p+0)的值是:3
*(p+1)的值是:6
*(p+2)的值是:5
*(p+3)的值是:8
*(p+4)的值是:9

4)数组名不一定会被解释为地址

在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。

可以修改指针的值,但数组名是常量,不可修改。

3、一维数组用于函数的参数

1)指针的数组表示

在C++内部,用指针来处理数组。

C++编译器把 数组名[下标] 解释为*(数组首地址+下标)

C++编译器把 地址[下标] 解释为*(地址+下标)

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	int a[5] = { 3 , 6 , 5 , 8 , 9 };

	int* p = a;

	for (int i = 0; i < 5; i++) {
		cout << "* (p + i)=" << * (p + i) <<", ";    //数组名[下标] 解释为 *(数组首地址+下标)
		cout << "p[i]=" << p[i] << endl;  //地址[下标] 解释为*(地址+下标)
	}
}

//输出:
* (p + i)=3, p[i]=3
* (p + i)=6, p[i]=6
* (p + i)=5, p[i]=5
* (p + i)=8, p[i]=8
* (p + i)=9, p[i]=9

2)一维数组用于函数的参数

一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。

书写方法有两种:

void func(int* arr, int len);

void func(int arr[], int len);

注意:

在函数中,可以用数组表示法,也可以用指针表示法。

在函数中,不要对指针名用sizeof运算符,它不是数组名。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

// void func(int *arr,int len)
void func(int arr[],int len)
{
	cout << "sizeof(arr)的值是:" << sizeof(arr) << endl;   //8,因为此时arr是指针,永远是8,不是数组名
	for (int ii = 0; ii < len; ii++)
	{
		cout << "arr[" << ii << "]的值是:" << arr[ii] << endl;              // 用数组表示法操作指针。
		cout << "*(arr+" << ii << ")的值是:" << *(arr + ii) << endl;   // 地址[下标]  解释为  *(地址+下标)。
	}
}

int main()
{
	int a[] = {2,8,4,6,7,1,9};
	cout << "sizeof(a)的值是:" << sizeof(a) << endl;   //28
	func(a, sizeof(a) / sizeof(int));
}

4、用new动态创建一维数组

普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。

动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];

释放一维数组的语法:delete [] 指针;

注意:

  • 动态创建的数组没有数组名,不能用sizeof运算符。

  • 可以用数组表示法和指针表示法两种方式使用动态创建的数组。

  • 必须使用delete[]来释放动态数组的内存(不能只用delete)。

  • 不要用delete[]来释放不是new[]分配的内存。

  • 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。

  • 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。

  • 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。

  • 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。

  • 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存。

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int main()
{
	int *arr=new int[8];          // 创建8个元素的整型数组。

	for (int ii = 0; ii < 8; ii++)
	{
		arr[ii] = 100 + ii;                                         // 数组表示法。
		cout << "arr[" << ii << "]=" << *(arr + ii) << endl;        // 指针表示法。
	}

	delete[]arr;
}

5、一维数组的排序qsort

qsort()函数用于对各种数据类型的数组进行排序。

函数的原型:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

第一个参数:数组的起始地址。

第二个参数:数组元素的个数(数组长度)。

第三个参数:数组元素的大小(sizeof(数组的数据类型))。

第四个参数:回调函数的地址。

回调函数决定了排序的顺序,声明如下:

int compar(const void *p1, const void *p2);

1)如果函数的返回值< 0 ,那么p1所指向元素会被排在p2所指向元素的前面。

2)如果函数的返回值==0,那么p1所指向元素与p2所指向元素的顺序不确定。

3)如果函数的返回值> 0 ,那么p1所指向元素会被排在p2所指向元素的后面。

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

qsort()函数的其它细节:

- 形参中的地址用void是为了支持任意数据类型,在回调函数中必须具体化。

- 为什么需要第三个形参size_t size?

- size_t是C标准库中定义的,在64位系统中是8字节无符号整型(unsigned long long)。

 typedef unsigned long long size_t

- 排序的需求除了升序和降序,还有很多不可预知的情况,只能用回调函数。

示例:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

int compasc(const void* p1, const void* p2)         // 升序的回调函数。
{
	return *((int*)p1) - *((int*)p2);
}

int compdesc(const void* p1, const void* p2)       // 降序的回调函数。
{
	return *((int*)p2) - *((int*)p1);
}

int main()
{
	int a[8] = { 4,2,7,5,8,6,1,3 };

	qsort(a,sizeof(a)/sizeof(int),sizeof(int),compasc);                   // 对数组a进行升序排序。

	for (int ii = 0; ii < 8; ii++)
	{
		cout << "a[" << ii << "]=" << a[ii] << endl;
	}

	qsort(a, sizeof(a) / sizeof(int), sizeof(int), compdesc);            // 对数组a进行降序排序。

	for (int ii = 0; ii < 8; ii++)
	{
		cout << "a[" << ii << "]=" << a[ii] << endl;
	}
}

6、二维数组

一维数组的数学概念是线性表,二维数组的数学概念是矩阵。

1)创建二维数组

声明二维数组的语法:数据类型 数组名[行数][列数]

注意:数组长度必须是整数,可以是常量,也可以是变量和表达式。

C90规定必须用常量表达式指明数组的大小,C99允许使用整型非常量表达式。经测试,在VS中可以用用整型非常量表达式,不能用变量;但是,Linux中还可以用变量。

2)二维数组的使用

可以通过行下标和列下标访问二维数组中元素,下标从0开始。

二维数组中每个元素的特征和使用方法与单个变量完全相同。

语法:数组名[行下标][列下标]

注意:

  • 二维数组下标也必须是整数,可以是常量,也可以是变量。

  • 合法的行下标取值是:0~(行数-1)。

  • 合法的列下标取值是:0~(列数-1)。

3)二维数组占用内存的情况

用sizeof(数组名)可以得到整个二维数组占用内存空间的大小(只适用于C++基本数据类型)。

二维数组在内存中占用的空间是连续的。

4)二维数组的初始化

声明的时候初始化:

数据类型 数组名[行数][列数] = { {数据1,数据2 } ,{数据3,数据4 },...... };

数据类型 数组名[行数][列数] = { 数据1,数据2,数据3,数据4, ......};

数据类型 数组名[ ][列数] = { 数据1,数据2,数据3,数据4,......};

数据类型 数组名[行数][列数] = { 0 };  // 把全部的元素初始化为0。

数据类型 数组名[行数][列数] = { };   // 把全部的元素初始化为0。

注意:如果{}内不足数组长度个数据,剩余数据用0补全,但是,不建议这么用,你可能在数组中漏了某个值。如果想把数组中全部的元素初始化为0,可以在{}内只填一个0或什么也不填。

C++11标准可以不写等于号。

5)清空二维数组

用memset()函数可以把二维数组中全部的元素清零。(只适用于C++基本数据类型)

函数原型:void *memset(void *s, int c, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

6)复制二维数组

用memcpy()函数可以把二维数组中全部的元素复制到另一个相同大小的数组(没说多少维)。(只适用于C++基本数据类型)

函数原型:void *memcpy(void *dest, const void *src, size_t n);

注意,在Linux下,使用memcpy()函数需要包含头文件#include <string.h>

7、二维数组用于函数的参数

int* p;    // 整型指针。

int* p[3];  // 一维整型指针数组,元素是3个整型指针(p[0]、p[1]、p[2])。

int* p();   // 函数p的返回值类型是整型的地址。

int (*p)(int ,int);  // p是函数指针,函数的返回值是整型。

1)行指针(数组指针)

声明行指针的语法:数据类型 (*行指针名)[行的大小]; // 行的大小即数组长度。

int (*p1)[3];  // p1是行指针,用于指向数组长度为3的int型数组。

int (*p2)[5];  // p2行指针,用于指向数组长度为5的int型数组。

double (*p3)[5];  // p3是行指针,用于指向数组长度为5的double型数组。

一维数组名被解释为数组第0个元素的地址。

对一维数组名取地址得到的是数组的地址,是行地址。

2)二维数组名是行地址

int bh[2][3] = { {11,12,13},{21,22,23} };

bh是二维数组名,该数组有2个元素,每一个元素本身又是一个数组长度为3的整型数组。

bh被解释为数组长度为3的整型数组类型的行地址。

如果存放bh的值,要用数组长度为3的整型数组类型的行指针。

int (*p)[3]=bh;

int bht[4][2][3];

bht是三维数组名,该数组有4个元素,每一个元素本身又是一个2行3列的二维数组。

bht被解释为2行3列的二维数组类型的二维地址。

如果存放bht的值,要用2行3列的二维数组类型的二维指针。

int (*p)[2][3]=bht;

3)把二维数组传递给函数

如果要把bh传给函数,函数的声明如下:

void func(int (*p)[3],int len);

void func(int p[][3],int len);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2295635.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LLMs之DeepSeek r1:Logic-RL的简介、安装和使用方法、案例应用之详细攻略

LLMs之DeepSeek r1&#xff1a;Logic-RL的简介、安装和使用方法、案例应用之详细攻略 目录 Logic-RL的简介 1、Logic-RL的特点 2、性能 Logic-RL 的安装和使用方法 1、安装 2、使用方法 数据准备 基础模型 指令模型 训练执行 实现细节 Logic-RL的案例应用 Logic-RL…

【神经网络框架】非局部神经网络

一、非局部操作的数学定义与理论框架 1.1 非局部操作的通用公式 非局部操作(Non-local Operation)是该研究的核心创新点,其数学定义源自经典计算机视觉中的非局部均值算法(Non-local Means)。在深度神经网络中,非局部操作被形式化为: 其中: 1.2 与传统操作的对比分析…

22.[前端开发]Day22-CSS单位-CSS预处理器-移动端视口

1 CSS常见单位详解 CSS中的单位 CSS中的绝对单位&#xff08; Absolute length units &#xff09; CSS中的相对单位&#xff08; Relative length units &#xff09; 1.em: 相对自己的font-size&#xff1b;如果自己没有设置, 那么会继承父元素的font-size 2.如果font-size中…

URL调用本地Ollama模型

curl http://192.168.2.247:11434/api/generate -d "{ \"model\": \"deepseek-r1:8b\", \"prompt\": \"Who r u?\" ,\"stream\":false}" 连续对话

【python】matplotlib(animation)

文章目录 1、matplotlib.animation1.1、FuncAnimation1.2、修改 matplotlib 背景 2、matplotlib imageio2.1、折线图2.2、条形图2.3、散点图 3、参考 1、matplotlib.animation 1.1、FuncAnimation matplotlib.animation.FuncAnimation 是 Matplotlib 库中用于创建动画的一个…

ubuntu24.04安装布置ros

最近换电脑布置机器人环境&#xff0c;下了24.04&#xff0c;但是网上的都不太合适&#xff0c;于是自己试着布置好了&#xff0c;留作有需要的人一起看看。 文章目录 目录 前言 一、确认 ROS 发行版名称 二、检查你的 Ubuntu 版本 三、安装正确的 ROS 发行版 四、对于Ubuntu24…

接入 deepseek 实现AI智能问诊

1. 准备工作 注册 DeepSeek 账号 前往 DeepSeek 官网 注册账号并获取 API Key。 创建 UniApp 项目 使用 HBuilderX 创建一个新的 UniApp 项目&#xff08;选择 Vue3 或 Vue2 模板&#xff09;。 安装依赖 如果需要在 UniApp 中使用 HTTP 请求&#xff0c;推荐使用 uni.requ…

网络爬虫js逆向之异步栈跟栈案例

【注意&#xff01;&#xff01;&#xff01;】 前言&#xff1a; 1. 本章主要讲解js逆向之异步栈跟栈的知识&#xff08;通过单步执行调试&#xff09; 2. 使用关键字搜定位加密入口 3. 本专栏通过多篇文章【文字案例】的形式系统化进行描述 4. 本文章全文进行了脱敏处理 5. 详…

机器学习 - 需要了解的条件概率、高斯分布、似然函数

似然函数是连接数据与参数的桥梁&#xff0c;通过“数据反推参数”的逆向思维&#xff0c;成为统计推断的核心工具。理解它的关键在于区分“参数固定时数据的概率”与“数据固定时参数的合理性”&#xff0c;这种视角转换是掌握现代统计学和机器学习的基础。 一、在学习似然函…

【Spring】什么是Spring?

什么是Spring&#xff1f; Spring是一个开源的轻量级框架&#xff0c;是为了简化企业级开发而设计的。我们通常讲的Spring一般指的是Spring Framework。Spring的核心是控制反转(IoC-Inversion of Control)和面向切面编程(AOP-Aspect-Oriented Programming)。这些功能使得开发者…

[笔记] 汇编杂记(持续更新)

文章目录 前言举例解释函数的序言函数的调用栈数据的传递 总结 前言 举例解释 // Type your code here, or load an example. int square(int num) {return num * num; }int sub(int num1, int num2) {return num1 - num2; }int add(int num1, int num2) {return num1 num2;…

开放式TCP/IP通信

一、1200和1200之间的开放式TCP/IP通讯 第一步&#xff1a;组态1214CPU&#xff0c;勾选时钟存储器 第二步&#xff1a;防护与安全里面连接机制勾选允许PUT/GET访问 第三步&#xff1a;添加PLC 第四步&#xff1a;点击网络试图&#xff0c;选中网口&#xff0c;把两个PLC连接起…

(原创,可用)SSH实现内外网安全穿透(安全不怕防火墙)

目前有A、B终端和一台服务器&#xff0c;A、B机器不能直接访问&#xff0c;服务器不能直接访问A、B终端但是A、B终端可以访问服务器&#xff0c;这个场景很像我们有一台电脑在单位内网&#xff0c;外机器想访问内网系统&#xff0c;可能大家目前想到的就是frp之类穿透工具&…

第二节 docker基础之---镜像构建及挂载

查看当前镜像&#xff1a; [rootdocker ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE [rootdocker ~]#很明显docker是咱们新搭建的所以目前还没有镜像 1&#xff0c;搜索镜像&#xff1a; [rootdocker ~]# docker search centos 搜索镜像并过滤是官…

LLM学习笔记1——本地部署Meta-Llama-3.2-1B大模型

系列文章目录 参考博客 参考博客 文章目录 系列文章目录前言与调用一、部署要求二、实现步骤0.深度学习环境错误1&#xff0c;验证pytorch版本时提示以下问题&#xff1a;错误2&#xff0c;验证pytorch版本时提示以下问题&#xff1a;错误3&#xff0c;有时候还会提示你有一些…

AI安全最佳实践:AI应用开发安全评估矩阵(上)

生成式AI开发安全范围矩阵简介 生成式AI目前可以说是当下最热门的技术&#xff0c;吸引各大全球企业的关注&#xff0c;并在全球各行各业中带来浪潮般的编个。随时AI能力的飞跃&#xff0c;大语言模型LLM参数达到千亿级别&#xff0c;它和Transformer神经网络共同驱动了我们工…

deepseek+kimi自动生成ppt

打开deepseek官网&#xff0c;输入详细的需求&#xff0c;让他生成个ppt 接着deepseek开始思考生成了 接着复制生成了的内容 打开kimi粘贴刚才deepseek生成的内容 可以一键生成啦&#xff0c;下载编辑使用吧

《薄世宁医学通识50讲》以医学通识为主题,涵盖了医学的多个方面,包括医学哲学、疾病认知、治疗过程、医患关系、公共卫生等

《薄世宁医学通识50讲》是一门由薄世宁医生主讲的医学通识课程&#xff0c;该课程旨在通过深入浅出的方式&#xff0c;向广大听众普及医学知识&#xff0c;提升公众对医学的认知和理解。 晓北斗推荐-薄世宁医学通识 以下是对该课程的详细介绍&#xff1a; 一、课程概述 《薄世…

突破与重塑:逃离Java舒适区,借Go语言复刻Redis的自我突破和成长

文章目录 写在文章开头为什么想尝试用go复刻redis复刻redis的心路历程程序员对于舒适区的一点看法关于mini-redis的一些展望结语 写在文章开头 在程序员的技术生涯长河中&#xff0c;我们常常会在熟悉的领域中建立起自己的“舒适区”。于我而言&#xff0c;Java 就是这片承载…

优惠券平台(一):基于责任链模式创建优惠券模板

前景概要 系统的主要实现是优惠券的相关业务&#xff0c;所以对于用户管理的实现我们简单用拦截器在触发接口前创建一个单一用户。 // 用户属于非核心功能&#xff0c;这里先通过模拟的形式代替。后续如果需要后管展示&#xff0c;会重构该代码 UserInfoDTO userInfoDTO new…