Pascal 转 C++ 教程1

Author Avatar
Axell 8月 06, 2018
  • 在其它设备中阅读本文章

Pascal 转 C++ 教程1

C++ 程序结构、数据结构、语法、函数

一个标准的C++程序:

    #include <cstdio>
    #define maxn 1000   // 预处理指令
    using namespace std;
    int a[maxn];        // maxn被替换为1000
    int main() {        // 主函数
       int n;
       scanf("%d", &n);
       for (int i = 0; i < n; i ++)
           scanf("%d", &a[i]);
       for (int i = 0; i < n; i ++)
           printf("%d\n", a[i]);
       return 0;
    }

语法

花括号和分号

{相当于Pascal中的begin,}相当于Pascal中的end,语句和Pascal语言一样用;分隔,一般一行写一条语句。

注释

C++中有两种方法使用注释(不编译的内容)。
//后到行尾的内容为注释(和Pascal中的//一样)。
/**/中间的内容为注释,相当于Pascal中的{}

预处理指令(#开头)

头文件(#include)
宏定义(#define)

头文件

OI竞赛中会用到的标准头文件有两类:继承自C的头文件STL头文件。C的头文件一般以c开头,例如前面的cstdio

OI竞赛中可能会用到的头文件

  • cstdio:包含读入输出相关函数,可以记为c+std(standard)+io(input&output)。一般来说这个头文件必然要包含。
  • cstring:包含C-style字符串相关的函数。
  • cmath:包含数学相关的函数。
  • cstdlib:包括随机函数等。
  • ctime:获取时间当做随机函数的种子。
  • STL头文件:后面会详细介绍STL。

主函数

和Pascal中的主程序一样,是程序开始执行的地方。不同的地方是C++的主函数有返回值,类型为int,并且正常结束时应该返回0。
在其他函数中,也可以用cstdlib头文件中的函数exit(0)直接退出整个程序,其中的参数0表示的就是主函数返回值。

基本类型

  • 整型
  • 实型
  • 字符型
  • 布尔型

带符号整型

int、short、long、long long

  • intlong为32位带符号整型(Pascal中的longint),范围是-2^31~2^31-1
  • short为16位带符号整型(Pascal中的integer),范围是-2^15~2^15-1
  • long long为64位带符号整型(Pascal中的int64),范围是-2^63~2^63-1
  • int的最大值约为2.1×109,为首选整型类型。

无符号整型

在对应的带符号整型前面加上unsigned,也就是unsigned int、unsigned short、unsigned long、unsigned long long。其中unsigned int可以直接写为unsigned
无符号整型不能存储负数,但存储的最大值可以扩大一倍,例如unsigned的范围就是0~2^32-1。

实型、字符型和布尔型

  • C++中有floatdoublelong double三种实型,精度依次递增,一般使用double
  • char类型只能存储字符,计算机内部是用ASCII码表示字符的,实际的范围是0~255。字符常量使用单引号包围的。
  • bool型只能为truefalse

各种类型占用的字节数

  • 1字节:char、bool
  • 2字节:short、unsigned short
  • 4字节:int、unsigned、long、unsigned long、float
  • 8字节:long long、unsigned long long、double
  • 12字节:long double

各种类型的常量

  • int类型的常量:2、100等(不需要任何后缀)
  • double类型的常量:0.0、1.5、1e50等
  • char类型的常量:’a’、’d’等(将字符夹在单引号间)
  • bool类型的常量:true和false
  • unsigned类型的常量:50u、50U(在非负整数后加上u或U后缀)
  • long long类型的常量:1000ll、1000LL

其他进制的整型常量

  • 十六进制前加0x前缀,例如0x100f、0x100F(大写小写均可)。
  • 八进制前加0前缀,例如0144,它表示八进制的144,十进制的值是100。因此不要随便加多余的零,防止被当做八进制处理。

转义字符

某些特殊的字符可以用转义的方法表示。转义字符以反斜杠()开头,例如\n。下面是比较常见的五种转义字符:

  • \n 换行
  • \\ 反斜杠
  • \? 问号
  • \' 单引号
  • \" 双引号

定义基本类型的变量

  • 定义变量的方式为类型名+变量名:

    • int x1, y1;
    • char x2, y2;
    • double x3, y3;
    • bool x4, y4;
  • 定义变量时也可以直接指定初值:

    • int a = 10;
    • char b = ‘p’;
    • double c = 1.2;
    • long long d = 1000LL;
    • bool e = true;
  • 定义基本类型的变量

    • C++语言几乎可以在程序的任何地方定义变量,但定义的变量只有在定义之后才能使用,如果在定义处之前就使用会给出编译错误。

隐式类型转换

  • 假如将A类型的变量或常量赋值给B类型的变量,A类型就会被隐式地转换为B类型。

  • 两个不同类型进行二元运算时,小类型也会被隐式转换成大类型以便运算。

    int a = 100;
    long long b = a;
    // int类型的a被隐式转换为long long类型后赋值给b
    double x = 1.5;
    int y = 10;
    double z = x * y;
    // int类型的y被隐式转换为double类型后和x相乘

显式类型转换

  • 除了隐式类型转换,在需要的时候,我们也可以显式地对变量的类型进行转换。其实我也比较推荐大家尽量使用显式类型转换,这样可以避免某些可能的错误。显示转换的方法是(type)x,其中type是要转换成的类型。假如后面要转换的是一个表达式,需要加上括号,即(type)(a+b),这是由于运算符优先级的问题,后面会再次提到。

    int a = 1000000000;
    long long b = (long long)a * a;
  • 这里int类型的a被显式转换成long long类型后和int类型的a相乘,由于相乘的两个类型不同,后面int类型的a会被隐式转换成long long类型后再与前面的a相乘。这里不进行显式类型转换的话,由于相乘的两个类型都是int,就会发生溢出,不过溢出这种错误运行时并不会报错。

基本类型间的转换

  • 实型->整型:去除小数部分,仅保留整数部分
  • 整型->实型:无变化,精度可能会损失
  • 整型<->字符型:ASCII码
  • 布尔型->整型:true变成1,false变成0
  • 整型->布尔型:0变成false,其他变成true
注意有些转换可能会溢出,例如unsigned->int,long long->int等
  • 由于有隐式类型转换,将数字字符转换成数字可以直接写成ch-‘0’,类似的还有ch-‘a’,其中ch表示数字字符或小写字母字符。
  • 假如要从大类型转换为小类型(如long long转换为int、double转换为int),因为精度可能会损失,建议使用显式类型转换提示自己。

指针类型

指针是指向特定类型内存地址的类型。C++中的&运算符可以取出某个变量在内存中的地址,以便赋值给相应的指针类型。后面讲运算符时还会介绍通过new的方式给指针赋值。空指针的值为0(也可以用NULL表示)。

    int a = 100;
    int *b = &a;
    // b是一个指向int类型的指针
    // 并且将它初始化为a变量的地址
    *b = *b * 2;
    // 相当于a变量被乘2了
    int *c = 0; // c是一个空指针
    *c = *c * 2; // 运行到这条时会错误

引用类型

引用和指针有相似的地方,但更恰当的理解是给变量取别名。引用在定义时必须被恰当地初始化,并且之后就不能再修改成其他变量的别名。

    int a = 100;
    int &b = a;
    // b是一个int的引用类型
    // 并且它被初始化指向a
    b = b * 2;
    // 相当于a变量被乘2了
注意点
  • 引用指针类型指向的地址时,需要在指针变量名前加*,但引用是和普通变量一样直接使用。

  • 定义指针和引用变量时需注意,每一个指针或引用变量前都要加*&

    int *a, b, &c = b;
    // a是指向int的指针类型
    // b是普通的int类型
    // c是指向int的引用类型

const类型

和Pascal中一样,C++里也可以定义常量,适当地定义常量不仅方便编程,也能够使程序意义更明确

    const double pi = 3.141592653;
    const int inf = 1000000000;
    const char space = ' ';

常量定义后就不能够再修改,除了直接输入常量的值,也可以通过常量表达式定义常量。

    const double pi = acos(-1);
    const int inf = 10000 * 10000;

类型的别名

对于某些很长的类型名,可以通过typedef在使用类型之前取别名,例如:
typedef long long LL;
这样LL就可以直接表示long long类型了。
再例如:
typedef int* pint;
就可以用pint来表示指向int的指针了。

CPU的字长

  • CPU在单位时间内能一次处理的二进制数的位数。
  • 当前的CPU基本都支持64位字长,但由于操作系统是32位的,因此当前绝大多数64位CPU都运行在了32位字长的模式下。
  • C++中的int类型就是32位长的,也就是占用4字节(8个位为一个字节)。它和CPU的字长相同,可以获得最高的效率,是首选的整数类型。

原码、反码和补码

反码和补码是针对一个特定的原码来说的。反码就是将原码01取反,补码就是在反码的基础上再加1。
例如原码是00001111,反码就是11110000,补码就是11110001
计算机内部表示整数使用的是二进制的方法,但是int类型也可以为负整数,对于负整数,就需要用到反码和补码。
int类型总共有32位,其中第一位是符号位,假如为0就是非负整数,否则是负整数,后面31位用来记录数字的信息。下面为了方便叙述,将32位的整型简化为8位。
先考虑最高位是0的情况,那么这个数就是后面7位二进制表示的整数,例如(01010101)2=85。
继续考虑第二种情况:最高位为1。此时这个二进制用来表示一个负数,这个负数的绝对值就是相应补码表示的正整数。例如(10101010)2,先将所有位都取反,得到(01010101)2,再加上1得到(01010110)2=86。因此(10101010)2表示的整数是-86。
特别地,(11111111)2表示的是-1,(10000000)2表示的是-128。扩展到32位整数的话,全是1依然表示-1,第一位1后面全0表示-231。因此int类型能表示的最小值是-231,而不是-231+1。
最后的问题是为什么要用这种方式来表示负数?
大家可以自己验证一下,如果这样表示的话,加法减法时都无需考虑整数的符号,可以直接对二进制加减(第32位产生的进位需舍去),就能得到正确结果。
但如果运算结果超出了类型能表示的范围,由于最高位的进位被舍去,计算出来的结果就会发生错误,所以要根据题目的数据范围选择合适的整数类型,必要的时候还需要使用高精度。
还有特别需要注意,只有带符号的整型使用这种方式表示整数的,不带符号的整型是直接用二进制表示的(因为不需要表示负数)。

练习题1

    int a = 10;  // 正确
    const int b = a * 2;
    // 错误,常量必须是常量表达式
    int &c;
    // 错误,引用类型定义时必须初始化
    char *d = &a;
    // 错误,指针类型不匹配
    int x = 1000000000;  // 正确
    long long y = x * x;
    // 错误,没有进行显式类型转换,会导致溢出

练习题2

    int a = 0xffff;
    // 十六进制,a的值为65535
    int b = 0xffffffff;
    // 十六进制,注意二进制全1表示-1,因此b的值是-1[计算机字长为64位时`0xffffffffffffffff`=-1]
    double c = 3.141592653;
    int d = c, e = (int)c; // d和e的值都是3
    char f = 'e';
    int g = f - 'a'; // g的值是4
    double h = -1.2;
    int i = (int)h; // i的值是-1
    int &x = d, *y = &e;
    x ++; *y --; // d变成4,e变成2

运算

四则运算符、取模

+ - * /,这个大家都懂得对吧。其中/需要注意一下,如果相除的两个变量(常量)都是整型,就会进行整除操作,否则进行实数除法。C++中的取模操作符是%取模要求操作的两个类型都是整型

    int a = 10, b = 3;
    double c = 3.5;
    double d = a / c; // c是实型,因此是实型除法
    int e = a / b; // a,b都是整型,因此是整除
    double f = a / (double)b;
    // b被转换成实型,因此是实型除法
    int g = a / 4; // a和4都是整型,因此是整除
    double h = a / 4.0; // 4.0是实型,因此是实型除法

逻辑运算符

< > <= >= == != && || !
分别对应Pascal中的< > = <> and or not
到后面讲if语句时会具体举例
逻辑运算符两边的类型不同时,也会进行隐式类型转换
C++中的字符型(char)可以直接用在<、>、<=、>=、==、!=等逻辑比较中。例如a是char类型的变量,你可以用('a' <= a && a <= 'z')来判断a是否表示小写英文字母。

位运算

Pascal中也可以进行位运算,但是大家用的或许比较少。Pascal中的三种位运算是and or xor,C++中对应的运算符是&、|和^
位运算就是把整数转换为二进制后,每位进行相应的运算得到结果。 &只有当两个都为1时才为1,|只要两个中有一个1就是1,^只有当两个不同时才为1
例如:5=(101)2,6=(110)2,那么5&6=(100)2=4、5|6=(111)2=7、5^6=(011)2=3。
还有一类位运算是左移右移,Pascal中是shlshr,C++中是<<>>。左移右移的作用就是将二进制表示整体移动,并舍去不在范围内的部分。例如5<<1=(1010)2=10、5>>1=(10)2=2。实际上可以看出,左移k位相当于乘上2k,右移k位相当于整除2^k

&、|和&&、||

&和|是按位与和或,而&&和||是逻辑与和或,观察下面的程序片段:

    int a = 10, b = 0;
    int c1 = a & b, c2 = a && b;
    int d1 = a | b, d2 = a || b;

其中a&b是按位与,a&&b的话,因为逻辑与要求两个数都是bool类型,因此会将a和b隐式转换为bool类型,其中a被转换为true,b被转换为false,计算完了之后又会被隐式转换为int类型赋值给c2。
这样分析一下,c1、c2的值为0,d1的值为10,d2的值为1。

赋值运算符

赋值也是一种运算符,和Pascal中不同,C++中的赋值运算符是=,而不是:=。相应的,逻辑等于就是==,而不是=。这些细节在刚转C++的时候尤其要注意。
赋值还可以和二元运算符结合,例如a+=b就表示将a+b赋值给a。其他类似的运算符还有-=、*=、/=、%=、&=、|=、^=、<<=、>>=等。

表达式

C++中的表达式比Pascal中更广泛,表达式的主要特点就是它有“值”。
例如加法表达式a+b,它的值就是加法运算的结果。再例如逻辑表达式a>=b,它的值是逻辑运算的结果(true或false,也可以说0或1)。
更高级的还有x=y这样的赋值,它也算表达式,它的值就是x(或y)的值。诸如+=的赋值表达式的值为左边变量赋值后的值。
因为赋值也算作一种表达式,因此我们可以写出x=y=z这样的连赋值,要从右向左看这个表达式,它先把z的值赋值给y,再把y=z的值赋值给x,其实就是把z赋值给x和y。
在表达式后面加上分号就能成为一条语句。

++和–

自加自减可以简单地看成Pascal中的inc和dec函数,但是它们有一些区别,区别就在于前面说的表达式,自加和自减操作也有
自加和自减有两种形式:前缀后缀。假如x为int类型的变量,x++++x的含义是不同的,虽然都会将x加上1,但考虑下面的两个表达式:
y=x++和y=++x
第一个的话,是先执行y=x、再将x加上一,第二个是先将x加上一、再执行y=x。用表达式的值来理解的话,前缀++和–会返回操作过后的值,后缀++和–会返回操作之前的值。可以用“先用再加”和“先加再用”来形象记忆。

注意:++和–

++、–这两种运算符使用时需要很小心,有时会写出难以解释的表达式,例如:x = x / ++ x
按照运算符优先级,++高于/,因此先处理++ x运算。这里的问题是,除法两边都是x,第二个显然是加一后的x,但前面的x到底指加一前还是加一后呢?C++语法标准中认为这样的表达式有二义性,不同的编译器有可能给出不同的结果。因此在使用++和—运算符时要小心地避免这样的情况,要保证写出的表达式很容易被解释。基本上把握带有前缀后缀++或–的变量只在表达式中出现一次,就不会出现大问题了

逗号运算符

逗号可以分割多个表达式,并整体组成一个表达式,它的值为最后一个表达式的值,计算的时候按照从左到右的顺序。

    int a, b, c, d;
    // 这里是定义,和逗号表达式没关系
    d = (a = 10, b = 20, c = 30, a <= b && b <= c);
    // **由于逗号运算符的优先级比赋值低,所以最外面有括号**
    // 整个逗号表达式的值为a <= b && b <= c这个表达式的值,也就是1
    c = 100, d = 200;
    // 逗号表达式也可以简单起到将多个赋值合并为一条语句的作用

new和delete

new用来申请动态内存空间,返回值是一个指针。例如:
int *p = new int;
就会使p指向一个int类型。
对于动态申请的空间,可以用delete删除。也就是:
delete p;
注意delete操作并不会使p变为0,它只会清除p指向空间,最好下一句手动将0赋值给p(p=0)。

运算符优先级

排在前面的优先级较大,同一组的优先级相同

  • Group 1

    • ++、–:后缀自加自减
  • Group 2

    • !:逻辑否
    • ++、–:前缀自加自减
    • +、-:正负号
    • &:取地址
    • (type):显式类型转换
  • Group 3

    • *、/、%:乘法、除法、取模
  • Group 4

    • +、-:加法、减法
  • Group 5

    • <<、>>:按位左移、右移
  • Group 6

    • <、<=、>、>=:小于、小于等于、大于、大于等于
  • Group 7

    • ==、!=:逻辑等于、逻辑不等于
  • Group 8

    • &:按位与
  • Group 9

    • ^:按位异或
  • Group 10

    • |:按位或
  • Group 11

    • &&:逻辑与
  • Group 12

    • ||:逻辑或
  • Group 13

    • =、+=、-=、*=、/=、%=、&=、^=、|=、<<=、>>=:赋值运算符
  • Group 14

    • ,:逗号运算符

大家特别需要注意的是,位运算运算符的优先级都比较低(低于加法和减法,有些低于逻辑比较运算符),使用的时候一定要加上适当的括号

练习题

    int a = 10, b = 3;
    int c = a / b;  // c的值为3
    double d = a / b;  // d的值仍然为3
    double e = (double)a / b;  // e的值为3.333……
    int f = a | b, g = a && b;
    // f的值为11,g的值为1
    int h = a + b >> 2;
    // >>优先级比+低,h的值为3
    int i = a ++;
    // i的值为10,a的值变为11
    int j = -- b;
    // j的值为2,b的值变为2

读入、输出

C语言中的读入输出函数

由于C++是C语言的扩展,因此可以使用C语言头文件中的函数。C语言中的读入函数是scanf,输出函数是printf,它们都是从屏幕读入输出。
这两个函数都是先传入一个格式字符串,再给出要读入或输出的若干变量。
注意如果要使用C语言的读入输出函数,需要#include <cstdio>

scanf函数

scanf函数中的格式字符串用来指定依次读入的类型,例如下面的例子中%d表示以十进制方式读入一个int类型,%lf表示读入一个double类型。后面给的变量需要是指针类型,因此要在前面加&

    int a;
    double b;
    scanf("%d%lf", &a, &b);

(C++中的字符串是用双引号包围的)

格式字符
  • %d – int
  • %ld – long
  • %lld – long long (Linux)
  • %I64d – long long (Windows)
  • %u – unsigned
  • %lu – unsigned long
  • %llu – unsigned long long (Linux)
  • %I64u – unsigned long long (Windows)
  • %c – char
  • %f – float
  • %lf – double

printf函数

和scanf类似,需要提供格式字符串,但是后面直接提供变量的值,不需要加&操作符。

    int a = 10;
    double b = 2.5;
    printf("%d %.5lf\n", a, b);

对于实数类型,用上面的方式可以四舍五入保5位小数输出。因此上面程序片段的输出结果是:
10 2.50000
(因为格式字符串中间有个空格,输出时中间也就有空格,并且最后会换行(n)
printf函数中的格式字符串其实就是要打印的内容,只不过它把%d%c之类的格式字符替换成后面提供的参数。所以你可以用printf函数直接输出字符串:
printf("Hello World\n");

scanf函数和printf函数的返回值

scanf函数返回成功赋值的数据项数,读到文件末尾出错时则返回EOF。这里的EOF是在cstdio头文件中被定义的
printf函数返回实际输出的字符数。
下面的程序可以不停地读入n后输出直到文件结束。

    int n;
    while (scanf("%d", &n) != EOF)
       printf("%d\n", n);

C++语言的读入输出字符

  • char c=getchar();
  • putchar(c);

C++语言的读入输出流

cin/cout

由于C++是面向对象的编程语言,对于读入输出的处理也更加对象化。由于OI竞赛中并不需要对面向对象的编程思想有太多了解,这里也不展开细说,看看下面的代码应该就很容易理解C++如何从屏幕读入输出。

    int x;
    double y;
    cin >> x >> y;
    cout << x << ' ' << y << endl;

cincoutiostream头文件中定义好的类,因此要#include <iostream>后才能使用。读入数据时可以想象是从cin中拿出来,所以用>>运算符,输出时正好相反,因此使用<<运算符。最后的endl表示换行,也可以用’n’代替。

输出流控制

  • dec : 十进制输入输出

  • hex : 十六进制输入输出

  • oct : 八进制输入输出

  • setw(int width) : 设置场宽

    • 可以写为cout.width
  • ws : 提取空白符,即去掉 符

  • setprecision(int num) : 设置输出位数(包括整数、小数)

    • 使用fixed后,设置小数位数
    • 一直作用,直到再次设置
    • 可以写为cout.precision
  • endl : 换行,并刷新流

  • ends : 终止符

  • setfill(int ch) : 填充字符

    • 可以写为cout.fill()
  • put : 输出一个字符

    • 可以级联cout.put().put()…
  • left/right : 左/右对齐

cin.get()/cin.getline()

  • cin.get

    • 读入一个字符作为返回值
    • char c=cin.get;
  • cin.getline

    • 读入一个C-Style字符串
    • cin.getline(数组起始值,最大长度)
    • char c[80];<br>cin.getline(c,80); // cin.getline(c+1,79);从c[1]开始读
    • 相当于gets(数组起始值)

如何选择读入输出方式

C语言的scanf函数和printf函数需要提供格式字符串,而C++语言的cincout只需要提供变量,这样看起来cin和cout显得更方便。不过在实际情况中,cin和cout的效率非常低下,读入或输出2MB的文件就有可能会超时(时限1s的情况),并且cout对于实数是选择一个合适的精度,要手动控制比较麻烦。因此大部分C++选手在OI竞赛中使用scanf和printf进行读入输出操作。使用scanf和printf读入输出long long类型时需要特别注意使用的系统平台,用错格式符的话会产生无法正确读入输出的后果。
还有要说明的是,scanf、printf和cin、cout默认情况下是同步的,也就是说你可以混用两种读入输出方式。例如对用long long类型使用cin、cout读入输出,就不需要特别注意系统平台的差异。

特别注意:有时不得不大量读入long long,需要使用cin/cout时,为了加快读入速度,可以使用下边的方式:

    std::ios::sync_with_stdio(false);
    std::cin.tie(0);//此时不要用scanf/printf了

练习题

    printf("%d\n", 10.0);
    // 格式字符错误,应该用%lf
    printf("%.5lf\n", 5);
    // 格式字符错误,应该用%d
    int a, b;
    scanf("%d%d", a, b);
    // a和b前面缺少&

数据结构、常用函数

数组

数组有个很文艺的名称:随机存取线性表。其实数组就是多个相同类型的变量依次排在一段连续的内存中。C++语言中的数组长度不可变。下面的程序片段中定义了一个长度为10的int数组a,可用的下标是0..9(你只能指定数组的长度,不能像Pascal中那样指定下标的上下界)。

    int a[10];
    a[0] = a[9] = 1;

    a[10] = 1;
    // 数组下标越界,但运行时未必会给出异常
    int n;
    int b[n]; // 数组长度必须是常量
数组的长度

C++中数组的长度可以是变量,但是定义之后便不可改变。如果说用变量作为数组的长度,那么数组的长度为执行定义的语句时那个变量的值。一般来说,我们都将数组定义成全局变量,由于全局变量在主函数之前,就没法用读入的数作为数组长度定义数组,因此一般情况下还是将数组长度指定为常数,并且要看清题目数据范围,不要把数组开小

一维数组和指针的关系

由于数组相当于内存中一段连续的空间,因此将刚才定义的数组a画成下面的形式:
下面的箭头表示指向相应位置的指针。例如指向a[0]的指针就是a(或看成a+0),也就是说&a[0]==a。然后&a[1]==a+1,以此类推直到&a[9]==a+9。这也就是说,如果a是某种类型的数组,那么它可以看成这种类型的指针。

指针的加减法

前面解释一维数组和指针的关系时,已经用到了将一个指针和整数相加的运算,除此之外,两个指针还可以相减。例如下面的程序片段:

    int a[10];
    int *p = &a[2], *q = &a[5];
    int t = q - p;  // q - p的值为3

注意到p实际上就是a+2,q实际上就是a+5,因此很自然的能得出q-p=(a+5)-(a+2)=3。
除了将指针加上或减去一个整数以外,也能够对指针进行++和–。
总结一下指针加减法:一个指针可以加上或减去一个整数,两个指针可以相减得到一个整数。

再谈new和delete

new操作符其实可以申请多于一个指定类型的空间,例如下面的程序片段:
int *a = new int[10];
new int[10]申请了连续10个int类型,并返回了指向第一个int的指针,这其实和数组是一样的。将这个指针赋值给int指针a后,a就可以当做数组使用,也就是说a[5]=1这类的赋值都是合法的。你可以这样理解,编译器对于[]运算符,实际上是进行了指针的加法,也就是说a[5]被理解为*(a+5),所以只要a是指针类型,就能使用[]运算符,但是你要保证你给的下标是有意义的。
清理空间时,要使用这样的语句:delete[] a;
使用new申请空间时的长度可以是变量。也就是你也可以这样写:int *a = new int[n];
出于效率的考虑,多数情况下我们不用这样的方法代替数组。

高维数组

除了一位数组之外,C++也支持高维数组,直接看下面的程序片段:

    int a[10][10], b[10][10][10];
    a[1][1] = 1;
    b[1][1][1] = 1;

上面的程序片段中定义了一个二维数组a和一个三维数组b,所有下标的范围都是0..9
C++中的高维数组访问和Pascal不一样,不是用逗号分隔,而是用多对方括号。(其实Pascal中也支持这样的访问方式)

C风格字符串

C语言里用字符数组来存储字符串,后面讲到STL时会再谈到C++中的string类。
C风格的字符串可以表示为指向char的指针,也就是char。这个指针指向字符串第一个字符的位置,后面的字符就是依次往后加,如果遇到了字符’0’(ASCII码为0的字符),就表示字符串结束了,其中’0’不是字符串的一部分。
char *str = "abcd";
观察上面这条赋值语句。其中”abcd”是常量字符串,编译器在编译时,会在内存某块区域申请连续5个char,依次赋值为’a’、’b’、’c’、’d’、’0’,然后将指向’a’的指针赋值给str。
如果我们想要创建字符串,可以定义一个char数组,然后依次赋值。例如下面的程序片段,虽然str是长度为10的数组,但是str也可以看成char
类型,它的前五个字符正好能看成一个完整的字符串。

    char str[10];
    str[0] = 'a'; str[1] = 'b';
    str[2] = 'c'; str[3] = 'd';
    str[4] = '\0';

读入、输出C风格字符串

scanf函数和printf函数可以读入输出C风格的字符串,使用的格式字符是%s

    char str[10];
    scanf("%s", str);
    printf("%s\n", str);

由于str等价于指向char的指针,因此scanf中传递的参数是str,而不是&str。
还有scanf函数读入时,空白字符(空格、制表符、换行等)都会作为分隔符,并且它会在字符串最后一个字符后面加上'\0',也就是说长度为10的字符数组最多只能存储长度为9的字符串,所以开数组的时候要注意多开1.

其他读入输出函数

用scanf函数读入字符串时,遇到空白字符就会停下,有时候题目要求读入一行字符串,这时候我们就可以使用gets函数。

    char str[1000];
    gets(str);

gets(str)会将当前行剩下的字符依次存入str中,并在最后加上’0’,因此你需要确保str数组的长度是足够的。上面的程序片段中,str数组只能存储长度为999的字符串。
类似的,使用puts函数可以输出一个字符串(输出字符串后不会自动输出一个换行)。
还有getcharputchar可以读入输出单个字符。需要注意换行字符、空白字符都会被getchar读到。

    char str[1000];
    gets(str);
    puts(str);
    char ch;
    ch = getchar();
    putchar(ch);

cstring头文件

cstring头文件中包含一些处理C风格字符串的函数,主要介绍下面两个函数的用法:

  • unsigned strlen(char *str);
  • int strcmp(char str1, char str2);
strlen函数

strlen函数可以计算字符串的长度,它的原理其实就是从给定的指针开始往后,遇到’0’时停止。

    int a = strlen("abcd");  // a = 4
    char b[10];
    b[0] = 'a';
    b[1] = 'b';
    b[2] = 'c';
    b[3] = '\0';
    int c = strlen(b); // c = 3
strcmp函数

strcmp(str1, str2)用来比较字符串str1和str2,返回值类型为int,返回值为-1说明str1字典序比str2小,返回值为0说明相等,返回值为1说明str1字典序比str2大。

    char a[10], b[10];
    scanf("%s%s", a, b);
    int c = strcmp(a, b);
strcat函数

strcat(str1,str2)用来连接字符串str1,str2结果保存在str1中,所以记得要把数组开的足够大

    char a[]="12345",b[]="67890";
    strcat(a,b);  //TIPs:不可以写成 a=b;
    cout<<a;  //输出1234567890
strcpy函数

strcpy(str1,str2)用来将str2拷贝至str1中,注意数组大小

    char a[]="12345",b[]="67890";
    strcpy(a,b);
    cout<<a;  //输出67890

什么时候使用C风格字符串?

特别需要说明的是,你没法用运算符来进行C风格字符串的相关操作,包括赋值、比较、连接等。后面会讲到C++的string类,使得你可以用运算符来完成字符串的很多操作。
由于不能使用运算符对C风格字符串进行操作,因此对于字符串操作题,一般不使用C风格字符串,而会使用STL中的string类。
但是有些题目只是读入字符串做一些简单判断,或只需要读取字符串的字符,这种时候用C风格字符串可以获得最好的效率。

memset函数

memset函数常用来给一个数组清0。一般写法是如下形式:

    int a[100];
    memset(a, 0, sizeof(a));

memset的工作方式是每个字节赋值成第二个参数的值,第一个参数表示开始赋值的位置,第三个参数表示赋值的字节数。sizeof(a)可以算出a数组占用的字节数。
除了清零之外,也可以用memset函数对数组整体赋值其他的值。

    bool a[100];
    memset(a, true, sizeof(a));
    int b[100];
    memset(b, 255, sizeof(b));
    char c[100];
    memset(c, 'a', sizeof(c));

其中bool数组a被清成了true,字符数组c被清成了字符’a’。int数组b被清成了-1,因为每个int四个字节的每个字节都是全1,所以最终的值是-1。

memcpy函数

memcpy函数可以用来复制数组:

    int a[100], b[100];
    memcpy(a, b, sizeof(b));

第一个参数表示目标数组,第二个参数被复制的数组,第三个参数表示复制的字节数。上面的用法就相当于把b数组的内容复制到a数组。假如a数组长度小于b数组,就会出现问题。假如a数组长度大于b数组,只有a数组的前100个int会被复制成b数组的内容,后面的保持不变。一般只会对长度相同、类型相同的数组使用memcpy。

memset和memcpy的注意点

这两个函数都在头文件cstring中,因此使用时需要#include <cstring>
memset和memcpy中都用到了sizeof运算符(没错,它其实是运算符,sizeof(a)可以写成sizeof a),sizeof运算符会计算给定变量或常量占用的字节数。对于数组类型,它会计算出整个数组占用的字节数。需要注意的是,如果你是用new申请了一段空间后赋值给一个指针,sizeof这个指针会得到这个指针类型占用的字节数(32位系统下是4字节),而不会得到new申请的字节数。
如果不太明白可以看下面的例子:

    int *a = new int[10];
    int x = (int)sizeof(a); // x = 4
    int b[10];
    int y = (int)sizeof(b); // y = 4 * 10

同样的情况还发生在函数的参数中,后面还会再提到。

分清指针和数组名

数组名可以看成常量指针(不可修改),但和指针是有本质区别的。
sizeof一个数组名得到的是整个数组占用的字节数,而sizeof一个指针得到的是4(32位系统下指针类型占用4字节)。

    int a[10]; // 数组名
    int *b; // 指针
    int *c = new int[10]; // 指针!

    b = c; // 合法赋值
    a = c; // 非法赋值!!

初始化数组

定义数组时,通常不能确保数组元素的初始值是0这样的值,比较保险的方法是在使用数组前对数组元素的值进行正确的初始化。
前面已经说过了用memset对数组整体赋值,除此之外,对于一维数组也可以在定义时赋初始值。

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

数组a的元素依次被初始化为0、1、2、3、4。数组b的话,前三个元素被初始化为0、1、2,最后两个元素使用默认值0。也就是说如果提供的数字个数不够了,剩下的元素就会用默认值0。
初始化字符数组时,我们还可以直接用下面的形式:
char a[10] = "abc";
数组a的前三个元素会依次被赋值为’a’、’b’、’c’,后面的元素都是’0’。
需要注意的是数组的长度必须够存储初始化的字符串,也就是数组长度至少要是字符串长度加一

常量数组

有时候为了方便编程,我们会定义常量数组。例如迷宫里只能向四个方向走时,我们可以定义一个二维的位移量数组:

  `const int u[4][2] = `{`{0, 1}, {0, -1}, {1, 0}, {-1, 0}`}`;`  

定义一维常量数组时可以不指定数组的大小,编译器会根据你提供的内容算出数组的大小:

  `const int t[] = {1, 2, 3, 4, 5};`  

结构体

结构体和Pascal中的record类似,可以将一些不同的类型放在一起:

    struct person {
       char name[100];
       int age;
       bool sex;
    };                         //要有分号!!
    person Jack;               //定义方式1
    Jack.age = 30;             //访问方式2
    Jack.sex = true;
    person *Bob = new person;  //定义方式2
    Bob->age = 18;             //访问方式2
    Bob->sex = true;

从前面的程序片段中可以看出,struct相当于是用户创建了一种新类型,这里创建的struct类型名为person,有三个域name、age和sex。后面访问时用’.’来访问域。如果是指针的话,可以直接用->访问域,而不用先用’*’再用’.’。
需要注意结构体定义右花括号后要有分号,不过其实可以在这个分号之前定义这个结构体的变量。例如:

    struct person {
        // 这里和前面一样,为了节约篇幅省略
    } Jack, *Bob;    //定义方式3

枚举类型

C++中的枚举类型和Pascal中的类似。

    enum color {
       red, green, blue
    } x;
    color y;
    x = red;
    y = blue;

上面的程序片段中定义了枚举类型color,然后还定义了color类型的变量x和y。
枚举类型和int类型是可以相互转化的。实际上枚举类型中的每种取值对应了一个数字,默认情况按照定义时候的顺序从0开始,当然也是可以人为地指定每种取值对应的数值。
前面的例子中,red对应0,green对应1,blue对应2。
通过隐式或显式类型转换,可以把枚举类型转换为int,也可以把int转换为枚举类型。

练习题1

    int t[10]; // 正确
    t[10] = 1; // 错误,下标越界
    char a[10] = "abcd"; // 正确,初始化字符数组
    a = "abc";
    // 错误,数组名虽然是指针,但不可改变取值
    int *b = new int[10]; // 正确
    memset(b, -1, sizeof(b)); // 错误,sizeof(b)为4
    int c[100]; // 正确
    memset(c, -1, sizeof(c));
    // 正确,c中的元素都被初始化为-1
    char d[4] = "abcd"; // 错误,数组长度至少要为5

练习题2

    int a[5] = {5, 4, 3, 2, 1};
    printf("%d\n", *a); // 输出a[0],即5
    printf("%d\n", *(a + 3)); // 输出a[3],即2
    printf("%d\n", (int)sizeof(a)); // 输出20
    int *b = new int[5];
    printf("%d\n", (int)sizeof(b)); // 输出4
    char a[10] = "abcde";
    printf("%d\n", (int)strlen(a)); // 输出5
    printf("%d\n", (int)sizeof(a)); // 输出10

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。

本文链接:https://axell.wind-flower.cn/archives/32/