NOIP学习小站
西安交通大学附属中学航天学校

char数组存储字符串

tangyj626阅读(629)

我们的第一个“Hello World”程序使用cout输出了"Hello World"这一句话其实就是字符串。字符串就是连续的字符“串”起来的一段文本,可以看成是若干有序的字符的集合。通过前面的学习,可知char数组就是若干有序的字符的集合,所以可以使用char数组来存储字符串。

一、字符串常量

回顾“Hello World”程序代码:cout<<"Hello World";,语句中的"Hello World"就是一个字符串常量。字符串常量书写的时候用英文双引号引起来,例如:"Hello World""I love programming""In me\nthe tiger sniffs the rose.""A"都是字符串常量。要注意"A"'A'的区别,"A"是只包含一个字符的字符串,'A'是一个字符。

二、使用字符数组存储字符串

首先来回顾普通字符数组的用法。普通字符数组就是char类型的数组,数组元素的类型是char类型。

#include<iostream>
using namespace std;
int main()
{
    char s[6];
    //输入6个char存储到字符数组中 
    for(int i=0;i<=5;i++) cin>>s[i];
    //输出字符数组中的元素 
    for(int i=0;i<=5;i++) cout<<s[i];
    cout<<endl;
    //对字符数组中的元素进行重新赋值 
    s[0] = 'z';
    s[2] = s[1];
    s[3]++;
    for(int i=0;i<=5;i++) cout<<s[i];
    return 0; 
}

那么如何保存字符串变量呢?例如输入一个字符串,应该如何存储?字符串就是连续的字符“串”起来的一段文本,可以看成是若干有序的字符的集合,所以可以使用char数组来存储字符串。

例如语句 char str[6] = "Hello";,将字符串常量"Hello"存储到了长度为6的字符数组str中。语句执行后,字符数组str每个元素的情况如下:

下标012345
元素'H''e''l''l''o''\0'

可见字符串的每个字符按照顺序依次存储到字符数组中,特殊地是,存储字符串的字符数组末尾会自动加上一个“尾巴”——表示字符串结束的特殊字符'\0'(空字符,ASCII码值为0),这也是为什么"Hello"只有5个字符,却至少要用长度为6的字符数组来存储。这是C/C++的自动处理机制,C/C++是靠空字符'\0'来识别保存到字符数组中的字符串的结束位置。可以通过一个程序来测试:

#include<iostream>
using namespace std;
int main()
{
    char str[6] = "Hello";
    cout<<str<<endl;	//输出存储在字符数组中的字符串
	str[3] = '\0';	  //一句“恶意”代码
	cout<<str<<endl; 
    return 0; 
}

程序的运行结果如下:

Hello
Hel

执行了str[3] = '\0';语句后,str数组元素如下:

下标012345
元素'H''e''l''\0''o''\0'

这个时候再用cout输出存储在字符数组str中的字符串,因为C/C++是靠空字符'\0'来识别保存到字符数组中的字符串的结束位置,所以会认为存储在str中的字符串是"Hel",程序最后输出的内容也正是"Hel"

通过上面的分析可知:

  1. 可以使用字符数组存储字符串;
  2. 存储时,从字符数组的 0 号下标开始依次存储字符串的每个字符。C/C++会自动在字符数组末尾的地方加上空字符'\0',注意这是运行时自动实现的,不需要我们写代码去实现!
  3. 正因为如此,定义存储字符串的字符数组时,数组的长度至少是要存储的字符串的长度+1。如果开的char数组很大,依次存储完字符串的所有字符后,字符数组后面的元素都是空字符'\0'

三、输入输出字符串

使用字符数组存储字符串,可以直接使用cin将输入的字符串存入到字符数组中,此时也会自动在数组的末尾添加上空字符'\0'。也可以直接使用cout输出存储在字符数组中的字符串,会自动依次输出字符数组中的字符直到遇到'\0'为止(不会输出'\0')。

例如我们要输入一句不包含空格、长度不超过10的英语单词,要将单词的字母全部大写,可以这样处理:

#include<iostream>
using namespace std;
int main()
{
    char str[11];  //输入的字符串长度不超过10,存储字符串的字符数组长度至少为11
    cin>>str;
    cout<<str<<endl;
    for(int i=0;str[i]!='\0';i++){   //使用循环遍历字符数组存储字符串的元素
    	//小写字母转大写
    	if(str[i]>='a' && str[i]<='z') str[i] -= 'a'-'A';
    	cout<<str[i];
	}
    return 0; 
}

如果输入的字符串长度小于10,例如输入"Hello",那么字符数组内部的存储形式如下:

下标012345678910
元素'H''e''l''l''o''\0''\0''\0''\0''\0''\0'

因为'\0'相当于整数0,所以还可以将遍历存储在字符数组中的字符串写成:

#include<iostream>
using namespace std;
int main()
{
    char str[11];  //输入的字符串长度不超过10,存储字符串的字符数组长度至少为11
    cin>>str;
    cout<<str<<endl;
    for(int i=0;str[i];i++){    //循环条件直接用s[i]
    	//小写字母转大写
    	if(str[i]>='a' && str[i]<='z') str[i] -= 'a'-'A';
	}
    cout<<str<<endl;  //前面循环中已经将char数组中的小写字母改成了大写字母
    return 0; 
}

关于字符串更多的输入输出方式,后面的章节会详细介绍。

四、字符串和字符数组的区别

  1. 字符数组就是元素类型为char的数组;
  2. 字符串可以存储在字符数组中。例如定义字符数组时赋初值为常量字符串、通过cin直接将字符串输入到字符数组、通过cout直接输出字符数组时,字符数组都会被当成字符串处理。存储的时候会自动在字符数组末尾添加上一个表示字符串结束的空字符'\0'
操作字符数组 char s[100];用来存储字符串的字符数组 char s[101];
赋值对数组某个元素赋值,例如
s[0] = 'a';
可以直接在定义时将字符数组赋初值为一个字符串常量,例如
char s[101] = "Hello World";
当然也可以像左边那样对“存储字符串的字符数组”的某个元素赋值
输入输入到数组某个元素,例如
cin>>s[i];
可以直接输入一个字符串,例如
cin>>s;
当然也可以像左边那样输入到“存储字符串的字符数组”某个元素
输出输出数组某个元素,例如
cout<<s[i];
可以直接输出一个字符串,例如
cout<<s;
当然也可以像左边那样输出“存储字符串的字符数组”某个元素

五、字符串函数

其实应用程序中处理字符串的操作远远多于处理数字,就像cmath中有很多用来实现数学计算的函数一样,C/C++中提供了字符串处理函数来简化对字符串的基本处理,要使用这些函数,首先要引入cstring头文件。

1.strlen字符串长度

可以使用strlen函数求字符串的长度,参阅下面的程序:

#include<iostream>
#include<cstring>
using namespace std;
char str[1001];
int main()
{
    cin>>str;
    cout<<strlen(str)<<endl;
    int n = strlen(str);
    for(int i=0;i<n;i++){
    	cout<<str[i];
	}
    return 0; 
}

使用strlen函数求出字符串的长度,-1则是字符串最后一个元素的下标,这样可以实现反向遍历字符串:

#include<iostream>
#include<cstring>
using namespace std;
char str[1001];
int main()
{
    cin>>str;
    for(int i=strlen(str)-1;i>=0;i--){
    	cout<<str[i];
	}
    return 0; 
}

2.strcpy拷贝字符串

使用strcpy(s1,s2);可以将字符串s2的内容拷贝到s1中,注意这里没有返回值,是直接改变字符串s1的内容。例如要交换两个字符串的值,不能像数字那样使用交换器,可以使用strcpy来拷贝字符串:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char s1[11] = "Hello";
    char s2[11] = "World";
    cout<<s1<<" "<<s2<<endl;

    char tmp[11];
    strcpy(tmp,s1);
    strcpy(s1,s2);
    strcpy(s2,tmp);

    cout<<s1<<" "<<s2<<endl;
    return 0; 
}

3.strcat字符串拼接

使用strcat(s1,s2);可以将字符串s2的内容拼接到s1的末尾,注意这里没有返回值,是直接改变字符串s1的内容。

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char s1[21] = "Hello";
    char s2[21] = "World";
    cout<<s1<<endl;

    strcat(s1,s2);
    cout<<s1<<endl;
    return 0; 
}

4.strcmp字符串比较

strcmp(s1,s2);函数返回使用字典顺序比较字符串s1、s2的结果,如果s1==s2,返回值是0;如果s1>s2,返回值大于0;如果s1<s2,返回值小于0。所谓字典顺序就是英语字典里单词的排列顺序,也和字符串中字符的ASCII值有关系。

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    cout<<strcmp("hello","hello")<<endl;
    cout<<strcmp("a","A")<<endl;   	 //"a">"A"
    cout<<strcmp("he","hello")<<endl;   //"he"<"hello"
    cout<<strcmp("H","h")<<endl;	    //"H"<"h"
    cout<<strcmp("Hello","h")<<endl;	//"Hello"<"h"
    cout<<strcmp("H","hello")<<endl;	//"H"<"hello"
    return 0; 
}

来看一段程序代码:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char choice[11];
    cin>>choice;
    //依次处理字符串中的char,大写字母转换成小写 
    for(int i=0;choice[i]!='\0';i++){
    	if(choice[i]>='A' && choice[i]<='Z'){
    		choice[i] += 'a'-'A';
		}
	}
    if(strcmp(choice,"yes")==0){
    	cout<<"you inputed yes";
	}
    return 0; 
}

程序将输入的字符串全部转换成小写后再判断和"yes"是否相等。运行程序,分别输入"YES""Yes""yes"甚至是"yEs""yES"之类能够表示"yes"的字符串,if语句的条件都成立。

注意体会并掌握上面处理策略包含的“标准化”思想。

5.memset初始化函数

memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值,这个函数通常为新申请的内存做初始化工作。我们一般用来将数组的元素全部赋值为同一个值,memset函数的处理速度优于用循环重复赋值。

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    int a[5];

    cout<<sizeof(a)<<endl;    //sizeof(a)计算数组a的字节长度,这里结果是20
    
    memset(a,0,sizeof(a));    //将int数组a的每个元素赋值为0
    for(int i=0;i<5;i++) cout<<a[i]<<" ";
    cout<<endl;
    
    memset(a,-1,sizeof(a));   //将int数组a的每个元素赋值为-1
    for(int i=0;i<5;i++) cout<<a[i]<<" ";
    cout<<endl;
    
    memset(a,1,sizeof(a));    //注意:这里并不能将int数组a的每个元素赋值为1
    for(int i=0;i<5;i++) cout<<a[i]<<" ";
    cout<<endl;
    return 0; 
}

程序运行结果如下:

20
0 0 0 0 0
-1 -1 -1 -1 -1
16843009 16843009 16843009 16843009 16843009

分析结果可知:memset(a,1,sizeof(a));并没有达到预期的将数组a的所有元素赋值为1的效果。原因是memset是以字节为单位对连续内存空间赋值的,int是4个字节,每个int元素的每个字节会被赋值成0000001,那么每个int元素会被赋值为二进制数 00000001000000010000000100000001,也就是十进制数16843009。所以使用memset函数对int或者long long类型数组元素批量赋值,一般赋值成0或者-1能够达到效果,赋值为255也会被初始化为-1,其他值不能达到效果。对于int 或者long long类型数组,使用memset初始化,如果初始化成127,那么数组的每个元素会是接近 int 或long long类型上限的正整数;如果初始化成128,那么数组的每个元素会是接近 int 或long long类型下限的负整数;

如果我们将上面程序的int数组修改成char数组,那么能够将数组的元素批量赋值成-128~127范围内的任意整数,因为char正好是一个字节:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char a[5];    //char数组,每个元素一个字节 
    
    memset(a,0,sizeof(a));
    for(int i=0;i<5;i++) cout<<(int)a[i]<<" ";
    cout<<endl;
    
    memset(a,-128,sizeof(a));
    for(int i=0;i<5;i++) cout<<(int)a[i]<<" ";
    cout<<endl;
    
    memset(a,127,sizeof(a));
    for(int i=0;i<5;i++) cout<<(int)a[i]<<" ";
    cout<<endl;
    return 0; 
}

*6.memcpy内存拷贝函数

memcpy是C和C++使用的内存拷贝函数,函数使用方法是memcpy(destin,source,n);函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,即从源source中拷贝n个字节到目标destin中。与strcpy不同的是,strcpy只能复制字符串,而memcpy可以复制任意内容,例如任意类型数组、整型、结构体、类等。

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    int b[10];
    
    memcpy(b,a,sizeof(a));
    
    for(int i=0;i<10;i++) cout<<b[i]<<" ";
    return 0; 
}

注意:有一些字符串函数,例如实现大小写转换的strlwr、strupr,不是标准C库函数,只能在Dev C++中使用,在竞赛标准的 Linux gcc 环境下不能使用。

C++中的string类型字符串

tangyj626阅读(678)

使用字符数组来存储字符串,其实是C的做法,C++除了沿用这样的做法外,还引入了专门的数据类型string来处理字符串。string本质是类,所以有很多字符串处理方法。

因为string是C++中的类,而类是C++的高级用法,所以本节的内容有一定的难度,如果不能较好地理解,可以尝试先记忆介绍的使用方法。

一、C++中的string类型

C++中有专门的string类型来处理字符串,所以可以像int、double这些基本数据类型一样很简单地使用string类型存储字符串,使用string最好引入string头文件:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s;
    cin>>s;
    cout<<s<<endl;
    return 0; 
}

需要特别注意的是,通过 cin 输入 string,与 cin 输入数字相同,默认的分割符号是空格、回车、制表符,所以上面的程序如果输入Hello World,字符串 s 的结果是Hello,只会把空格前的内容输入到字符串 s 中。如果想将有空格的内容全部输入到字符串中,可以使用 getline(cin,s);,这样可以将一整行的内容输入到字符串中 。

二、使用string中的某个字符

可以像使用数组下标访问数组元素那样通过下标来使用string中的某个字符,不仅仅是取出来使用,甚至可以修改:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s = "Hello";
	cout<<s<<endl;

	cout<<s[0]<<s[1]<<s[2]<<s[3]<<s[4]<<endl;

	s[0] = 'X'; 
	cout<<s<<endl;    
    return 0; 
}

三、string与字符数组字符串的相互转换

C使用字符数组来存储字符串,C++除沿用这样的方法外,还有专门的string类型来处理字符串。可以通过代码来相互转换:

#include<iostream>
#include<cstring>
#include<string>
using namespace std;
int main()
{
    char str[15] = "Hello World";
	string s(str);  //使用字符数组初始化string 
	cout<<s<<endl;
	
	s = "Hi";
	//s.c_str()方法返回string变量s的字符数组存储形式
	//再通过strcpy函数拷贝到str数组中 
	strcpy(str,s.c_str());
	cout<<str<<endl;
    return 0; 
}

四、string常用方法

1.string相比较char数组存储字符串的优势

string可以认为是一种数据类型,支持string变量直接赋值成另一个变量:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s = "Hello";
	string ss = s;
	cout<<ss;    
    return 0; 
}

string类型支持+运算,表示字符串拼接:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s = "Hello";
	cout<<s+" World"<<endl;
	s += " World";
	cout<<s<<endl;
    return 0; 
}

string类型支持>、>=、<、<=、==、!=这些比较运算,string字符串比较时规则和char数组存储的字符串规则一致:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s;
    cin>>s;
    if(s=="yes") cout<<"you inputed YES";
    return 0; 
}

2.string的初始化

string本质是类,有丰富的构造函数(后面会有所介绍),所以初始化string变量的方式很多:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string s1 = "Hello";    //通过赋值语句初始化
    cout<<s1<<endl;
    
    string s2("Hello");     //用字符串常量初始化
    cout<<s2<<endl;
    
    string s3(s2);          //用其它字符串变量初始化
    cout<<s3<<endl;
    
    char str[6] = "Hello";
    string s4(str);         //用存储在字符数组的字符串初始化
    cout<<s4<<endl;
    return 0; 
}

3.string字符串长度

使用string的length方法或者size方法都能获取字符串的长度。使用方法参见下面的程序:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string s;
	cin>>s;
	cout<<s.length()<<endl;		//使用length方法获取字符串长度 
	cout<<s.size()<<endl;		  //使用size方法获取字符串长度
	
	//与string长度相关的变量可以使用int类型,但推荐使用size_t或者string::size_type
	size_t n = s.length();		 //使用size_t类型变量存储获取到的字符串长度
	//使用size_t类型控制变量遍历string的每个字符 
	for(size_t i = 0;i<n;i++){
		cout<<s[i];
	}
    return 0; 
}

4.遍历string所有元素

可以使用for循环来依次枚举string元素的下标,实现正向以及逆向遍历string的元素:

#include<iostream>
#include<string>
using namespace std;
int main(){
	string s;
	cin>>s;
	//size_t 在标准Linux环境下本质上是 unsigned long long(无符号长整型)
	//正向遍历
	for(size_t i = 0;i<s.length();i++)
		cout<<s[i];
	cout<<endl;
	
	//反向遍历:控制变量是size_t类型,这里循环条件不能用 i>=0
	//原因是当size_t类型变量i值为0时,执行i--后,i值会变成很大的一个整数(string::npos)
	//所以这里条件用 i!=string::npos
	for(size_t i = s.length()-1;i!=string::npos;i--)
		cout<<s[i]; 
	cout<<endl;
	return 0;
} 

还可以使用STL(标准模版库)中的迭代器实现string元素的遍历(后续进阶学习内容):

#include<iostream>
#include<string>
using namespace std;
int main(){
	string s;
	cin>>s;
	
	//正向遍历 
	string::iterator it = s.begin(); 
	for(;it != s.end();it++)
		cout<<*it;
	cout<<endl;
	
	//反向遍历 
	string::reverse_iterator rit = s.rbegin(); 
	for(;rit != s.rend();rit++)
		cout<<*rit;
	cout<<endl;
	return 0;
} 

*5.修改string内容

可以通过多种语句或者调用方法修改string的内容:

#include<iostream>
#include<string>
using namespace std;
int main ()
{
    string s = "Hello World";
    cout<<1<<":"<<s<<endl;
    
    //通过下标方式修改字符串某个字符,字符串也会变化 
    s[5] = '+'; 
    cout<<2<<":"<<s<<endl; 
    
    //assign方法重新赋值,效果与 s="Nice to meet you";一致,但执行方式不同 
    s.assign("Nice to meet you");
	cout<<3<<":"<<s<<endl; 
	
	string st="Hello";
	//swap方法交换s与st的值
	s.swap(st); 
	cout<<4<<":"<<s<<" "<<st<<endl; 
	
	//append方法末尾追加字符串
	//效果与 s = s+" World";或者 s+=" World";一致,但执行方式不同 
	s.append(" World");
	cout<<5<<":"<<s<<endl;
	
	//push_back方法在字符串末尾追加字符 
	s.push_back('!');
	cout<<6<<":"<<s<<endl;
	
	//insert方法在字符串中插入字符串 
	//在第0个字符前插入字符串 
	s.insert(0,"Say ");
	cout<<7<<":"<<s<<endl;
	
	//replace方法替换字符串指定区间的内容
	//s从第4个开始的一共5个字符被替换成"Hi"
	s.replace(4,5,"Hi"); 
	cout<<8<<":"<<s<<endl;
	//s从第0个开始的一共4个字符被替换成"",实现的是删除的效果 
	s.replace(0,4,""); 
	cout<<9<<":"<<s<<endl;
	
	//erase方法删除字符串指定区间的所有字符
	//从第2个字符开始总共删除6个字符
	s.erase(2,6); 
	cout<<10<<":"<<s<<endl;
	
	//clear方法清空字符串,变成空字符串"" 
	s.clear();
	cout<<11<<":"<<s<<endl;
	cout<<12<<":"<<s.size()<<endl;

    return 0;
}

程序运行结果如下:

1:Hello World
2:Hello+World
3:Nice to meet you
4:Hello Nice to meet you
5:Hello World
6:Hello World!
7:Say Hello World!
8:Say Hi World!
9:Hi World!
10:Hi!
11:
12:0

*6.字符串查找方法

string有丰富的字符串查找方法,包括find、rfind、find_first_of、find_last_of、find_first_not_of、find_last_not_of等方法可以实现丰富的查找:

#include<iostream>
#include<string>
using namespace std;
int main ()
{
    string s = "Hello World";
    
    //在s中查找字符'o'的位置
    cout<<1<<":"<<s.find('o')<<endl;
    
    //在s中从第5个字符开始查找字符'o'的位置
	cout<<2<<":"<<s.find('o',5)<<endl;
	
	//在s中查找字符串"or"出现的位置
	cout<<3<<":"<<s.find("or")<<endl;
	
	//在s中从第8个字符查找字符串"or"出现的位置,查不到返回string::npos
	cout<<4<<":"<<s.find("or",8)<<endl;
	if(s.find("or",8)==string::npos)
		cout<<5<<":"<<"Not found"<<endl;
	
	//在s中逆向查找字符'o'的位置
	cout<<6<<":"<<s.rfind('o')<<endl;
	
	//在s中从第5个字符开始,逆向查找字符'o'的位置
	cout<<7<<":"<<s.rfind('o',5)<<endl;
	
	//在s中逆向查找字符串"or"出现的位置
	cout<<8<<":"<<s.rfind("or")<<endl;
    return 0;
}

程序运行结果如下:

1:4
2:7
3:7
4:4294967295
5:Not found
6:7
7:4
8:7
#include<iostream>
#include<string>
using namespace std;
int main ()
{	
	string str("This is one string for example");
	
	//str中第一个字符'i'出现的位置
	cout<<1<<":"<<str.find_first_of('i')<<endl;
	
	//str中从第3个字符开始,第一个'i'出现的位置
	cout<<2<<":"<<str.find_first_of('i',3)<<endl;
	
	//字符串"oiehtT"任意一个字符在str中第一次出现的位置
	cout<<3<<":"<<str.find_first_of("oiehtT")<<endl;
	
	//从第3个字符开始,字符串"oiehtT"任意一个字符在str中第一次出现的位置
	cout<<4<<":"<<str.find_first_of("oiehtT",3)<<endl;
	
	//逆向查找:str中第一个字符'e'出现的位置
	cout<<5<<":"<<str.find_last_of('e')<<endl;
	
	//逆向查找:字符串"le"中任意一个字符在str中第一次出现的位置 
	cout<<6<<":"<<str.find_last_of("le")<<endl;
    return 0;
}

程序运行结果如下:

1:2
2:5
3:0
4:5
5:29
6:29
#include<iostream>
#include<string>
using namespace std;
int main ()
{	
	string str = "iiiiekpkiiieylmnqqqqq";
	
	//str中第一个不是'i'的字符的位置
	cout<<1<<":"<<str.find_first_not_of('i')<<endl;
	
	//str中第一个不是"eik"字符串中任意字符的位置
	cout<<2<<":"<<str.find_first_not_of("eik")<<endl;
	
	//str中从第7个字符开始查找第一个不是"eik"字符串中任意字符的位置 
	cout<<3<<":"<<str.find_first_not_of("eik",7)<<endl;
	
	//逆向查找:str中第一个不是字符'q'的字符出现的位置
	cout<<4<<":"<<str.find_last_not_of('q')<<endl;
	
	//逆向查找:str中第一个不是字符串"nqm"的任意字符出现的位置
	cout<<5<<":"<<str.find_last_not_of("nqm")<<endl;
    return 0;
}

程序运行结果如下:

1:4
2:6
3:12
4:15
5:13

*7.获取字符串子串

可以通过string的substr方法获取字符串的子串(连续的一部分)

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string s = "Hello World";
	cout<<s<<endl;
	
	//从s下标2开始直到结尾的子串 
	string s1 = s.substr(2);	//结果是:llo World
	cout<<s1<<endl;
	
	//从s下标2开始,长度为5的子串 
	string s2 = s.substr(2,5);	//结果是:llo W 
	cout<<s2<<endl; 
	
	//从s下标0开始直到结尾的子串 
	string s3 = s.substr(0);	//结果是:Hello World
	cout<<s3<<endl;
	return 0;
}

字符串的输入输出

tangyj626阅读(584)

通过前面的学习可知,可以使用字符数组来存储字符串,也可以直接使用C++的string类型来存储字符串。但是如果要按行输入带有空格的字符串,只是简单地使用cin和scanf是不能实现的。

本小节重点研究字符串的输入,重点处理按行输入有空格的字符串的情况。

现实生活中,编程解决实际问题时,要处理的字符和字符串数据其实远多于数字类型;在信息学竞赛中,字符串的处理也是一个重点。

一、字符串的输入输出

不管是用字符数组存储字符串还是直接使用C++的string类型存储字符串,都可以使用cin输入字符串、cout输出字符串:

#include<iostream> 
using namespace std;
int main()
{
    char str[1010];
    cin>>str;
    cout<<str<<endl;
    return 0;
}
#include<iostream>
#include<string> 
using namespace std;
int main()
{
    string str;
    cin>>str;
    cout<<str<<endl;
    return 0;
}

不能使用scanf和printf输入输出string类型字符串,但是可以使用scanf和printf输入输出存储在字符数组中的字符串,还可以使用puts函数输出存储在字符数组中的字符串:

#include<cstdio>
int main()
{
    char str[1010];
    scanf("%s",str);  //不能用&str 
    printf("%s\n",str);
    puts(str);	    //puts函数输出字符串,输出字符串后会自动换行 
    return 0;
}

二、按行输入有空格的字符串

不管是使用scanf还是cin输入字符串,空格依然是多个数据间的分隔符,如果想输入一行包括空格的字符串,直接简单地使用scanf和cin是不能实现的,还是上面的代码:

#include<iostream> 
using namespace std;
int main()
{
    char str[1010];
    cin>>str;
    cout<<str<<endl;
    return 0;
}
#include<iostream>
#include<string> 
using namespace std;
int main()
{
    string str;
    cin>>str;
    cout<<str<<endl;
    return 0;
}

想输入一行包括空格的文本,例如Hello World,运行程序,输入Hello World,输出结果都是Hello,可知str只接收到了Hello World空格前的Hello。那么如何按行输入有空格的字符串呢?

1.使用字符数组存储字符串的情况

与puts函数输出用字符数组存储的字符串相对应的是,使用gets函数将字符串输入到字符数组中保存,gets函数能够按行输入,即使行内有空格也不受影响:

#include<cstdio> 
int main()
{
    char str[1010];
    gets(str);
    puts(str);
    return 0;
}

输入Nice to meet you!,输出结果如下:

Nice to meet you!

可知gets函数能够很好地实现按行输入,即使行内存在空格。

注意:gets函数有内存泄漏的风险,并且竞赛使用的C++版本已经移除了gets函数,所以不推荐使用!

对于C,推荐使用fgets函数读入一行,不过要注意fgets函数往往会导致接收到的字符串末尾会有多余的换行符'\n',需要特殊处理:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010];    //最好多开几个
	fgets(str,sizeof(str),stdin);    //注意fgets的使用方法
	
	int n = strlen(str);
	//处理末尾可能额外附带的'\n'
	if(n && str[n-1]=='\n') str[--n] = '\0';
	
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

还可以使用C++中的cin.getline读入一行:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010];
	cin.getline(str,sizeof(str));    //注意cin.getline的使用方法	
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

2.使用string类型的情况

使用string类型存储字符串,可以使用getline函数读入一行内容,即使行内有空格也不受影响:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str;
	getline(cin,str);
	cout<<str<<" "<<str.length()<<endl;
    return 0;
}

3.使用getchar重复接收输入直到遇到换行

可以使用getchar函数每次读取一个字符存入到字符数组中,直到读到换行符'\n'为止:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010],ch;
	int tot = 0;
	while(true){
	    ch = getchar();
	    if(ch=='\n') break;
	    str[tot++] = ch;
	}
	str[tot] = '\0';
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

上面的代码可以进一步精简:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010],ch;
	int tot = 0;
	while((ch = getchar()) != '\n') str[tot++] = ch;
	str[tot] = '\0';
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

其实当输入的数据量很大的时候,使用getchar的效率反而是更高的!与输入字符getchar()函数对应的是putchar()函数,用于输出字符,例如putchar(ch);,将以字符的形式输出变量ch。

三、字符串输入出现异常的处理

来看一个简单的问题,输入正整数\(n\),然后输入\(n\)个包含空格的字符串,输出每个字符串的长度。很自然写出下面的程序(分别是string和char数组存储字符串):

#include<iostream>
#include<string>
using namespace std;
int main()
{
    int n;
    string str;
    cin>>n;
    for(int i=1;i<=n;i++){
    	getline(cin,str);
    	cout<<str.length()<<endl;
	}
    return 0; 
}
#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
    	fgets(str,sizeof(str),stdin);
    	int len = strlen(str);
    	//处理末尾可能额外附带的'\n'
    	if(len && str[len-1]=='\n') str[--len] = '\0';
    	cout<<strlen(str)<<endl;
	}
    return 0; 
}

运行上面的程序,输入测试数据与结果如下:

尝试输入数据:

2
Jim Green
Han Meimei

执行结果:

运行时会发现,输入了第一个字符串回车,还没有输入第二个字符串,程序就结束运行了。是什么原因导致出现这样意外的运行结果呢?

仔细分析运行结果可知,循环体内第一次getline(或fgets)输入的字符串竟然是一个空字符串""(所以输出长度为0),应该是前面输入整数并回车导致循环体内第一次不能正常输入字符串,getline(或fgets)会将整数后回车前的内容(其实就是一个空字符串)输入到字符串中,这样奇葩的处理方式真的是防不胜防呀!

好在我们在运行测试的过程中就能发现这个问题并可以想办法解决。这里也给我们一个提醒:字符和字符串的输入一定要小心,往往会出现防不胜防的异常输入情况,特别是在字符字符串与其他类型数据混合输入的时候。

回到上面的问题,其实我们可以在cin>>n;语句后添加一句getchar();来接收一个字符,就能很好地避免上面出现字符串输入异常的情况。

*四、scanf函数的神奇用法:扫描字符集合

其实scanf函数还有很多神奇的用法,例如使用scanf("%[^\n]",str);也能输入有空格的一行字符串,格式符中^表示非,^\n表示不是回车一直读,遇到回车结束。

char str[1010];
scanf("%[^\n]",str);
printf("%s %d",str,strlen(str));

但是用"%[^\n]"作为输入格式符,要多次输入字符串,例如输入两个字符串,要注意接收回车,看下面的程序:

char c[1001],s[1001];
scanf("%[^\n]",c);
scanf("%[^\n]",s);

上面的程序,不接收回车,那么数组s就输不进去,想输进去就要在第1个输入语句scanf("%[^\n]",c);之后加个getchar();接收回车; getchar();也可以换成scanf("%*c");scanf("%*c");的作用是读入一个字符但不保存到变量。

所以多组输入带空格的字符串存储到字符数组可以这样写:

char str[1010];
while(scanf("%[^\n]%*c",str)!=EOF){
    printf("%s %d\n",str,strlen(str));
}

*五、扩展:扫描字符集合

扫描字符集合是scanf函数用于字符串读取的一个工具,它可以比%s更灵活地控制读取过程,具体如下:

1.%[]的中括号中需要填写一个正则表达式,用于指明只读取哪些字符或者不读取哪些字符

2.当中括号内的内容不是以^开头的时候,表示只读取在中括号中出现的内容,当遇到第一个没有出现的字符时,就停止读取,并把目前已经读取的内容保存到对应的字符数组中,例如:

char s[110];
scanf("%[0-9]",s); //只读取数字

假设输入为:123a456,那么上面的scanf将把123读取并保存到s数组中,其余的a456将遗留在缓冲区中。

如果把上面的scanf()调用改为如下形式:scanf("%[13579]",s); 并且输入如下:123,那么将只读取1,并把它做为字符串保存到s 中,其余字符将遗留在缓冲区中,因为第二个字符'2'没有出现在扫描集中,所以不再继续读取。

3.如果扫描集的第一个字符是^,那么读取规则就变成了只读取没有出现在扫描集中的字符,遇到第一个出现在扫描集中的字符时,读取即告停止,例如: scanf("%[^0-9]",s); 这个调用将只读取非数字字符,遇到数字字符时读取停止,如果输入的是:abc2020noip,那么将读取abc到s数组,其余的字符将遗留在缓冲区中。

字符串例题——判断字符串相等

tangyj626阅读(215)

问题:判断两个字符串是否相等。

分析:使用char数组存储字符串,可以直接使用strcmp函数来比较两个字符串;如果使用string存储字符串,那么可以直接使用==来判断是否相等:

#include<iostream>
#include<cstring>
using namespace std;
char str1[1010],str2[1010];
int main()
{
	cin>>str1>>str2;
	if(strcmp(str1,str2)==0)
		cout<<"YES";
	else
		cout<<"NO";
    return 0;
} 
#include<iostream>
#include<string>
using namespace std;
int main()
{
	string str1,str2;
	cin>>str1>>str2;
	if(str1 == str2)
		cout<<"YES";
	else
		cout<<"NO";
    return 0;
} 

其实不使用strcmp函数,直接用for循环依次比较字符串中的字符也能实现:

#include<iostream>
using namespace std;
char str1[1010],str2[1010];
int main()
{
	cin>>str1>>str2;
	int i;
	for(i=0;str1[i] && str2[i];i++){
		if(str1[i] != str2[i]) break;
	}
	if(str1[i]=='\0' && str2[i]=='\0')
		cout<<"YES";
	else
		cout<<"NO";
    return 0;
} 

问题变形:判断两个字符串是否相等(不区分大小写)。

分析:先将两个字符串都转换成小写(或者大写),再比较是否相等。

#include<iostream>
#include<cstring>
using namespace std;
char str1[1010],str2[1010];
int main()
{
	cin>>str1>>str2;
	//字符串str1转小写 
	for(int i=0;str1[i];i++){
    	if(str1[i]>='A' && str1[i]<='Z'){
    		str1[i] += 'a'-'A';
		}
	}
	//字符串str2转小写
	for(int i=0;str2[i];i++){
    	if(str2[i]>='A' && str2[i]<='Z'){
    		str2[i] += 'a'-'A';
		}
	}
	if(strcmp(str1,str2)==0)
		cout<<"YES";
	else
		cout<<"NO";
    return 0;
} 

字符串例题——求逆序数

tangyj626阅读(187)

问题:输入一个正整数\(n\)(\(1 \le n \le 10^{16}\)),计算并输出\(n\)的逆序数。正整数\(n\)的逆序数就是\(n\)倒着念的整数(高位不能有多余的0),例如123的逆序数是321,5700的逆序数是75。

分析:使用循环拆分出存储在long long类型变量\(n\)每位上的数字,一边拆分一边组装逆序数。(可参阅【循环结构例题一】中例题【五、逆序数】的详细讲解)

#include<iostream>
using namespace std;
int main()
{
	long long n,m = 0;
	cin>>n;
	while(n){
		m = m*10 +n%10; 
		n /= 10;
	}
	cout<<m<<endl;
	return 0;
} 

问题变形:输入的正整数\(n\)的位数不超过100位,计算并输出\(n\)的逆序数。

分析:正整数\(n\)的位数不超过100位,不能再使用int、long long这样的基本数据类型存储正整数\(n\),可以使用字符串来存储。那么此时逆序数就是字符串逆序输出,但是要避免输出高位多余的0。

#include<iostream>
#include<cstring>
using namespace std;
char num[110];   //额外多开一些 
int main()
{
	cin>>num;
	//逆序找到第一个不是'0'的字符
	int i = strlen(num)-1; 
	while(num[i]=='0') i--;
	
	//从下标i开始逆序输出 
	while(i>=0){
		cout<<num[i--];
	}
	return 0;
} 

此外还可以使用string来存储整数,并且使用string的find_last_not_of方法逆向找到第一个不是'0'的字符的位置:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string num;
	cin>>num;
	//用string的find_last_not_of方法返回逆向第一个不是'0'的字符的位置 
	int i = num.find_last_not_of('0');
	while(i>=0) cout<<num[i--];
	return 0;
} 

当然,也可以使用find_last_of方法返回字符串逆序查找"123456789"中任意字符的第一处位置,其实就是逆向第一个不是'0'的字符的位置:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string num;
	cin>>num;
	//用string的find_last_of方法返回num逆序查找"123456789"中任意字符的第一处位置 
	int i = num.find_last_of("123456789");
	while(i>=0) cout<<num[i--];
	return 0;
} 

字符串例题——判断回文字符串

tangyj626阅读(200)

问题:判断不包含空格且长度不超过1000的字符串是否是回文字符串。所谓回文字符串就是顺着念、倒着念完全相同的字符串。

分析:对于字符串\(str\),设其长度为\(n\),那么\(str\)是回文字符串必须要满足:str[0] == str[n-1]str[1] == str[n-2]……(str[i] == str[n-1-i])等条件。反之,如果找到任意一个不相等的字符对,那么肯定不是回文字符串。

#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
	cin>>str;
	int n = strlen(str);
	int find = 0;           //标记是否找到不相等的字符对 
	for(int i=0;i<n;i++){   //循环条件可以修改为i<=n/2
		if(str[i] != str[n-1-i]){
			find = 1;
			break;
		}
	}
	if(find) cout<<"NO";
	else cout<<"YES";
	return 0;
} 

使用C++的string来解决问题参考代码如下:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string str; 
	cin>>str;
	size_t n = str.length();
	int find = 0;           	//标记是否找到不相等的字符对 
	for(size_t i=0;i<n;i++){   	//循环条件可以修改为i<=n/2
		if(str[i] != str[n-1-i]){
			find = 1;
			break;
		}
	}
	if(find) cout<<"NO";
	else cout<<"YES";
	return 0;
} 

还可以用两个变量 \(i,j\) 来表示要判断相等字符对的下标:

#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
	cin>>str;
	for(int i=0,j=strlen(str)-1;i<j;i++,j--){
		if(str[i] != str[j]){
			cout<<"NO";
			return 0;     //提前结束程序
		}
	}
	cout<<"YES";
	return 0;
} 

使用C++的string来解决问题参考代码如下:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string str;
	cin>>str;
	for(size_t i=0,j=str.length()-1;i<j;i++,j--){
		if(str[i] != str[j]){
			cout<<"NO";
			return 0;     //提前结束程序
		}
	}
	cout<<"YES";
	return 0;
} 

字符串例题——统计单词数量

tangyj626阅读(178)

问题:统计长度不超过1000的字符串中的单词数量。字符串中的单词用一个或者多个空格隔开。

分析:输入的字符串包含空格,不能简单地使用cin或者scanf输入。可以直接遍历字符串中的字符,遍历过程中统计单词首字符的数量,单词首字符的特点是:当前字符不是空格,上一个字符是空格。

#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
	int cnt = 0;
	char last = ' ';	//记录上一个字符,体会这里默认值设置成空格字符的用途 
	fgets(str,sizeof(str),stdin);
	//遍历字符串中的字符
	for(int i=0;str[i];i++){      //条件str[i]是str[i]!='\0'的简写
		//如果上一个字符是空格、当前字符不是空格,则找到一个新单词
		if(last == ' ' && str[i] != ' '){
			cnt ++;
		}
		//last更新为当前字符,在下一次循环中就成了“上一个字符”
		last = str[i];
	}
	cout<<cnt<<endl;
	return 0;
} 

编译运行,发现基本上能解决问题,但是有一个小小的BUG——如果输入的字符串末尾有空格,那么统计出来的单词数量比实际单词数量多1个。原因是fgets函数输入字符串时,字符串的末尾可能会额外多一个'\n'。如下图所示,此时程序也会认为找到了一个新单词。

#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
	int cnt = 0;
	char last = ' ';	//记录上一个字符,体会这里默认值设置成空格字符的用途 
	fgets(str,sizeof(str),stdin);
	//遍历字符串中的字符,考虑了fgets输入字符串末尾可能额外附带的'\n'
	for(int i=0;str[i] && str[i] != '\n';i++){
		//如果上一个字符是空格、当前字符不是空格,则找到一个新单词
		if(last == ' ' && str[i] != ' '){
			cnt ++;
		}
		//last更新为当前字符,在下一次循环中就成了“上一个字符”
		last = str[i];
	}
	cout<<cnt<<endl;
	return 0;
} 

当然也可以使用string存储字符串,使用getline函数输入有空格的字符串到string:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	int cnt = 0;
	char last = ' ';	//记录上一个字符,体会这里默认值设置成空格字符的用途 
	string str;
	getline(cin,str); 
    //遍历string的每个字符 
	for(size_t i=0;i<str.length();i++){
		//如果上一个字符是空格、当前字符不是空格,则找到一个新单词
		if(last == ' ' && str[i] != ' '){
			cnt ++;
		}
		//last更新为当前字符,在下一次循环中就成了“上一个字符”
		last = str[i];
	}
	cout<<cnt<<endl;
	return 0;
} 

其实本问题最简单的处理方法是使用循环输入字符串的方式将用空格隔开的单词依次输入:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	int cnt = 0;
	string str;
    while(cin>>str) cnt++;
	cout<<cnt<<endl;
	return 0;
} 

虽然在命令行状态下运行使用标准输入时,需要额外输入CTRL+Z并回车才能结束输入,和前面程序输入回车直接结束输入不相同,但是测评时从文件读入字符串,上述程序的输入效果时一致的。

字符串例题——统计字符串中某单词出现次数

tangyj626阅读(207)

问题:统计字符串\(str\)中单词\(word\)出现的次数。已知字符串中的单词用一个空格或者多个空格隔开。

输入格式:有2行,第1行是字符串\(str\),第2行是不带空格的单词\(word\)。

分析:使用string来存储字符串\(str\)和单词\(word\)。可以通过string的find方法查找子串的方式统计单词出现次数,例如字符串和单词内容如下:

nice to meet you and how are you thank you very much
you

初步想法是用find方法统计单词you在字符串nice to meet you and how are you thank you very much 出现次数:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	int cnt = 0;
	string str,word;
    getline(cin,str);
    cin>>word;
    //查找单词word第一次出现的位置 
    size_t pos = str.find(word);
    while(pos != string::npos){   //查找到
    	cnt++;
    	//从pos+word.length()起继续查找单词word第一次出现的位置 
    	pos = str.find(word,pos+word.length());
	}
	cout<<cnt<<endl;
	return 0;
} 

但是考虑下面的情况:

nice to meet you and how are you thank you and your friends very much
you

因为your中也完整出现了单词you,也会被统计到。find方法是查找子串,如果字符串\(str\)中某个单词中完整出现了单词\(word\)(此外还有其他字符),用上面的方法也会认为单词\(word\)出现了一次导致统计结果出错!那么该如何解决问题呢?

其实我们可以用find方法统计字符串" "+str+" "中单词" "+word+" "出现的次数,这样处理就能有效避免上面的问题(注意体会并掌握这种处理方法):

#include<iostream>
#include<string>
using namespace std;
int main()
{
	int cnt = 0;
	string str,word;
    getline(cin,str);
    str = " "+str+" ";
    cin>>word;
    word = " "+word+" ";
    //查找单词word第一次出现的位置 
    size_t pos = str.find(word);
    while(pos != string::npos){
    	cnt++;
    	//从pos+word.length()起继续查找单词word第一次出现的位置 
    	pos = str.find(word,pos+word.length());
	}
	cout<<cnt<<endl;
	return 0;
} 

如果我们用字符数组存储字符串,那么该如何统计单词出现次数呢?其实我们使用前一小节例题中的方法将字符串中的每个单词找出来:

#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
    char last = ' ';    //记录上一个字符,体会这里默认值设置成空格字符的用途
    fgets(str,sizeof(str),stdin);
    
    int n = strlen(str);	//n是字符串长度 
	int start = 0;	      //start记录单词首字符下标 
    
    //处理fgets输入字符串末尾可能额外附带的'\n' 
	if(n && str[n-1]=='\n') str[--n] = '\0';
	
    for(int i=0;i<=n;i++){   //注意这里的条件是i<=n,而不是一般遍历字符串的i<n
	 
        //如果上一个字符是空格、当前字符不是空格,则找到一个新单词
        if(last == ' ' && str[i] != ' '){
            start = i;       //记录找到的单词的首字符下标
        }else if(last != ' ' && (str[i] == ' ' || str[i] == '\0')){ //单词结束
        	//从start到i-1是一个单词
        	for(int j=start;j<i;j++) cout<<str[j];
        	cout<<endl;
		}
		
        //last更新为当前字符,在下一次循环中就成了“上一个字符”
        last = str[i];
    }
    return 0;
} 

那么只需要判断每次找出来的单词是不是\(word\)就能统计单词\(word\)的数量了:

#include<iostream>
#include<cstring>
using namespace std;
char str[1010],word[1010];
int main()
{
	int cnt = 0; 
    char last = ' ';    //记录上一个字符,体会这里默认值设置成空格字符的用途
    fgets(str,sizeof(str),stdin);
    cin>>word;
    
    int n = strlen(str);	//n是字符串长度 
	int start = 0;		  //start记录单词首字符下标 
    
    //处理fgets输入字符串末尾可能额外附带的'\n' 
	if(n && str[n-1]=='\n') str[--n] = '\0';
	
    for(int i=0;i<=n;i++){   //注意这里的条件是i<=n,而不是一般遍历字符串的i<n
	 
        //如果上一个字符是空格、当前字符不是空格,则找到一个新单词
        if(last == ' ' && str[i] != ' '){
            start = i;
        }else if(last != ' ' && (str[i] == ' ' || str[i] == '\0')){ //单词结束 
        	int j,k;
        	//从start到i-1是一个单词
        	//比较字符串str的start到i-1的字符与word的字符是否依次相等 
			for(j=start,k=0;j<i && word[k];j++,k++){
        		if(str[j] != word[k]) break;
        	}
        	//全部匹配相等,并且单词都匹配结束,那么匹配到单词word 
        	if(j==i && word[k]=='\0') cnt++;
		}
		
        //last更新为当前字符,在下一次循环中就成了“上一个字符”
        last = str[i];
    }
    cout<<cnt<<endl;
    return 0;
} 

字符串例题——删除单词后缀

tangyj626阅读(162)

问题:给定一个单词,如果单词以er、ly或者ing后缀作为结尾,则删除该后缀,否则不做任何处理。输出单词删除er、ly、ing后缀的结果(输入保证保证删除后缀后的单词长度不为0)。

分析:将单词存储在字符数组中,判断字符数组尾部的2个字符或者3个字符是否是后缀,如果是可以设置后缀第一个字符为字符串结束标志'\0'

#include<iostream>
#include<cstring>
using namespace std;
char s[1001];
int main()
{
    cin>>s;
    int len = strlen(s);
    if(len>=3 && s[len-1]=='g' && s[len-2]=='n' && s[len-3]=='i'){
        //手动设置存储字符串的字符数组某个元素为终止字符,会改变字符串的长度             
        s[len-3]='\0';
    }
    if(len>=2 && (s[len-1]=='y' && s[len-2]=='l' || s[len-1]=='r' && s[len-2]=='e')){
        s[len-2]='\0';
    }
    cout<<s;
    return 0; 
}

问题变形:上面的问题提出新的要求,删除后缀之后继续判断是否存在后缀,如果存在删除后缀,一直这样处理直到没有后缀为止。

分析:通过循环处理依次所有的后缀,使用标记变量标记某一次是否找到后缀,如果找到后缀,那么还需要继续执行循环;如果没有找到后缀,那么就可以退出循环了。

#include<iostream>
#include<cstring>
using namespace std;
char s[1001];
int main()
{
	cin>>s;
	int len = strlen(s);
	bool find = true;	//记录是否发现要删除的后缀,初始值为true 
	while(find){
		find = false;	//判断前默认没有发现,设置为false 
		if(len>=3 && s[len-1]=='g' && s[len-2]=='n' && s[len-3]=='i'){
			len -= 3; 
			s[len] = '\0';
			find = true;	//本次发现后缀 
		}
		if(len>=2 && (s[len-1]=='y' && s[len-2]=='l' || s[len-1]=='r' && s[len-2]=='e')){
			len -= 2;
			s[len] = '\0';
			find = true;	//本次发现后缀 
		}
		//执行到这里,如果find == true(也就是本次发现了要删除的后缀),那么循环还会继续执行(因为循环的条件就是find == true)
		//否则(find == false),那就没有发现要删除的后缀了,会自动跳出循环 
	}	
	cout<<s;
	return 0; 
}

当然也可以使用while(true)死循环,在循环体中没有发现后缀的时候使用break跳出循环:

#include<iostream>
#include<cstring>
using namespace std;
char s[1001];
int main()
{
    cin>>s;
    int len = strlen(s);
    while(true){
        bool find = false;    //判断前默认没有发现,设置为false 
        if(len>=3 && s[len-1]=='g' && s[len-2]=='n' && s[len-3]=='i'){
            len -= 3;
            s[len] = '\0'; 
            find = true;    //本次发现后缀
        }
        if(len>=2 && (s[len-1]=='y' && s[len-2]=='l' || s[len-1]=='r' && s[len-2]=='e')){
			len -= 2;
			s[len] = '\0';
			find = true;	//本次发现后缀 
		}
        //本次没有发现后缀,break强制退出循环 
        if(!find) break;
    }    
    cout<<s;
    return 0; 
}

也可以使用C++的string来解决问题:

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string s;
	cin>>s;
	while(true){
		bool find = false; 
		if(s.length()>=2 && (s.rfind("er")==s.length()-2 || s.rfind("ly")==s.length()-2)){
			s = s.substr(0,s.length()-2);
			find = true;
		}
		if(s.length()>=3 && s.rfind("ing")==s.length()-3){
			s = s.substr(0,s.length()-3);
			find = true;
		}
		if(!find) break;
	}
	cout<<s<<endl;
	return 0;
} 

上面程序中,代码 s.rfind("er") 是用来逆向查找字符串"er"在字符串 s 中出现的位置,很显然,如果s.rfind("er")==s.length()-2,那么字符串肯定是以"er"结尾的("er"是字符串 s 的后缀)。s.substr(start,n)用来获取字符串s中从 start 开始的 n 个字符的子串。

字符串例题——P1957 口算练习题

tangyj626阅读(206)

通过本例题介绍sscanf和sprintf函数的使用方法。字符串的处理是应用程序的重要操作,也是竞赛的考点,在编程时需要灵活处理。

这是一道常规问题,但是由于每行中输入的数据个数不一致,数据的处理是一个难点。进一步分析问题可知,每行有3个数据时,第一个数据是字符;每行有2个数据时,第一个数据是整数。这里应该用按行输入字符串的方式输入每行的算式。

每行算式按照字符串输入后,该如何进一步处理呢?这里可以使用一个有意思的函数sscanf,sscanf和scanf都是格式化输入函数,不过sscanf是指定从某个字符串中“输入”数据。printf函数其实有返回值,就是输出的内容的长度:

#include<cstdio>
#include<cstring>
char str[110],one[20];
int main()
{
    int n,a,b,c,len;
    char op;
    scanf("%d\n",&n);
    for(int i=1;i<=n;i++){
        cin.getline(one,sizeof(one));  //使用cin.getline输入一行有空格的字符串

        //sscanf指定从某个字符串中“输入”数据
        if(one[0]>='a' && one[0]<='c'){
            sscanf(one,"%c%d%d",&op,&a,&b); 
        }else{
            sscanf(one,"%d%d",&a,&b);
        }

        //printf函数的返回值(输出内容的长度)保存到len变量中
        switch(op){
            case 'a':c=a+b;len=printf("%d+%d=%d",a,b,c);break;
            case 'b':c=a-b;len=printf("%d-%d=%d",a,b,c);break;
            case 'c':c=a*b;len=printf("%d*%d=%d",a,b,c);break;
        }
        printf("\n%d\n",len);
    }
    return 0;
}

与sscanf类似,sprintf和printf都是格式化输出函数,不过sprintf是将数据“输出”保存到字符串中。

#include<cstdio>
#include<cstring>
char str[110],one[20];
int main()
{
	int n,a,b,c;
	char op;
	scanf("%d\n",&n);
	for(int i=1;i<=n;i++){
		fgets(one,sizeof(one),stdin);  //使用fgets输入一行有空格的字符串(本题不用关心输入后末尾可能存在的\n)

		//sscanf指定从某个字符串中“输入”数据
		if(one[0]>='a' && one[0]<='c'){
			sscanf(one,"%c%d%d",&op,&a,&b); 
		}else{
			sscanf(one,"%d%d",&a,&b);
		}

		//sprintf将数据格式化“输出”保存到字符串中
		switch(op){
			case 'a':c=a+b;sprintf(str,"%d+%d=%d",a,b,c);break;
			case 'b':c=a-b;sprintf(str,"%d-%d=%d",a,b,c);break;
			case 'c':c=a*b;sprintf(str,"%d*%d=%d",a,b,c);break;
		}
		printf("%s\n%d\n",str,strlen(str));
	}
    return 0;
}

当然,也可以扫描字符串生成操作数\(a\)和\(b\),计算得出结果\(c\),使用数的拆分方法计算结果\(c\)的位数,不过程序会显得繁琐:

#include<cstdio>
#include<cstring>
char str[110],one[20];
int main()
{
	int n,a,b,c,la,lb,lc,j;
	char op;
	scanf("%d\n",&n);
	for(int i=1;i<=n;i++){
		cin.getline(one,sizeof(one));  //使用cin.getline输入一行有空格的字符串

		j = 0;	//j记录字符串中第1个整数的开始下标 
		if(one[0]>='a' && one[0]<='c') op = one[0],j = 2;
		
		//生成a并计算a的位数
		a = la = 0; 
		while(one[j]>='0' && one[j]<='9') a = a*10+one[j]-'0',j++,la++;
		j++;
		
		//生成b并计算b的位数
		b = lb = 0;
		while(one[j]>='0' && one[j]<='9') b = b*10+one[j]-'0',j++,lb++;
		
		switch(op){
			case 'a':c=a+b;printf("%d+%d=%d",a,b,c);break;
			case 'b':c=a-b;printf("%d-%d=%d",a,b,c);break;
			case 'c':c=a*b;printf("%d*%d=%d",a,b,c);break;
		}
		
		lc = 2;	//lc是c的位数+运算符+等于符号+结果是负数时的负号 
		if(c<0) c = -c,lc++;	//结果是负数,特殊处理 
		
		if(c==0) lc++;	//结果为0,特殊处理 
		else{
			while(c) lc++,c /= 10;	//计算c的位数 
		}
		
		printf("\n%d\n",la+lb+lc);
	}
    return 0;
}

如果使用string来处理本问题的字符串,可以使用字符串流stringstream来将数据输出到字符串流或者从字符串流中输入数据到变量中:

#include<iostream>
#include<sstream>
using namespace std;
int main()
{
	int n,a,b,c;
	cin>>n;
	char op;
	
	//前面输入了整数,后面循环体中要用getline按行输入string,这里用下面的语句来做特殊处理
	//否则第一次循环的getline获取到的是一个换行符
	string tmp = "\n";
	getline(cin,tmp);

	for(int i=1;i<=n;i++){
		string s,ans;
		getline(cin,s);
		
		//字符串流ss1中的内容初始化成字符串s 
		stringstream ss1(s);
		if(s[0]>='a' && s[0]<='c'){
			//从字符串流ss1中输入字符op和两个整数a和b 
			ss1>>op>>a>>b;
		}else{
			//从字符串流ss1中输入两个整数a和b 
			ss1>>a>>b;
		}
		
		//将算式输出到字符串流ss2中
		stringstream ss2; 
		ss2<<a;
		switch(op){
			case 'a':ss2<<"+";c = a+b; break;
			case 'b':ss2<<"-";c = a-b; break;
			case 'c':ss2<<"*";c = a*b; break;
		}
		ss2<<b<<"="<<c;
		//从字符串流ss2中输入一个string变量ans  
		ss2>>ans;
		cout<<ans<<endl<<ans.length()<<endl;
	}
	return 0;
} 

字符串例题——字符串数组

tangyj626阅读(172)

问题:输入\(n\)(\(1 \le n \le 1000\))位小组成员姓名(姓名中只有英文字符,每个姓名不超过10个字符),逆序输出所有姓名。

分析:使用string数组存储所有成员姓名,然后逆序输出数组中的string即可。

#include<iostream>
#include<string>
using namespace std;
string names[1000]; 
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>names[i];
    }
    for(int i=n-1;i>=0;i--){
        cout<<names[i]<<endl;
    }
    return 0; 
}

此外,如果用字符数组存储所有姓名,需要使用二维数组。此时二维char数组的每一行是一个字符串(一位成员的姓名)。

#include<iostream>
#include<cstring>
using namespace std; 
char names[1000][15];
int main()
{
    int n,len;
    cin>>n;
    for(int i=0;i<n;i++){
        //names[i]是二维char数组的第i行,用来存储一个字符串 
        cin>>names[i];
    }
    for(int i=n-1;i>=0;i--){
        cout<<names[i]<<endl;
    }
    return 0; 
}

问题变形:输入的姓名中可以出现空格。结合前面小节的内容可知,如果使用string存储姓名,那么需要用getline函数输入姓名;如果使用char数组存储姓名,那么可以使用fgets函数输入姓名。

#include<iostream>
#include<string>
using namespace std;
string names[1000]; 
int main()
{
    int n;
    cin>>n;
    getchar();   //不要忘了这一句,否则后面输入字符串会出现异常 
    for(int i=0;i<n;i++){
        getline(cin,names[i]);
    }
    for(int i=n-1;i>=0;i--){
        cout<<names[i]<<endl;
    }
    return 0; 
}
#include<iostream>
#include<cstring>
using namespace std; 
char names[1000][15];
int main()
{
    int n,len;
    cin>>n;
    getchar();   //不要忘了这一句,否则后面输入字符串会出现异常 
    for(int i=0;i<n;i++){
        //names[i]是二维char数组的第i行,用来存储一个字符串 
        fgets(names[i],sizeof(names[i]),stdin);
        int len = strlen(names[i]);
    	//处理末尾可能额外附带的'\n'
    	if(len && names[i][len-1]=='\n') names[i][--len] = '\0';
    }
    for(int i=n-1;i>=0;i--){
        cout<<names[i]<<endl;
    }
    return 0; 
}

使用内置函数

tangyj626阅读(400)

C/C++中有很多可以直接调用的内置函数,例如math头文件中提供的\(pow\)、\(sqrt\)、\(abs\)等数学函数,又例如stdio头文件中提供的格式化输入输出函数\(scanf\)和\(printf\)。所谓内置函数,可以理解为编程语言已经定义好的“工具”,我们先引入函数所在的头文件,然后就可以按照“工具的使用规则”调用函数。

使用内置函数时,需要先引入函数所在的头文件(函数在这个头文件里被定义)否则会出现编译错误:

#include<iostream>
#include<cmath>     //使用pow、sqrt等数学函数,需要引入cmath头文件  
using namespace std;
int main()
{    
    cout<<pow(60,2)<<endl;
    cout<<sqrt(9)<<endl;
    return 0;
} 
#include<iostream>
#include<cstring>   //使用字符串函数,引入cstring头文件 
using namespace std;
int main()
{	
	char str[] = "Hello World";
	cout<<strlen(str)<<endl;
	cout<<str<<endl;	
    return 0;
}
#include<iostream>
#include<algorithm>  //使用sort函数排序,需要引入算法库algorithm头文件  
using namespace std;
int main()
{    
    int a[10] = {1,5,9,0,2,4,8,3,7,6};
    sort(a,a+10);        //sort函数用于排序 
    for(int i=0;i<10;i++) cout<<a[i]<<" ";
    cout<<endl;    
    return 0;
} 

如果我们要使用很多函数(或者后面介绍的STL中的容器、算法等),那么很可能要引入大量的头文件,有些头文件的名称还很复杂不好记忆,这个时候要全部记住这些头文件也是一件痛苦的事情啊。好在我们可以使用一个“万能”头文件来避免这样尴尬的局面:#include<bits/stdc++.h>

#include<bits/stdc++.h>    //引入万能头文件
using namespace std;
int main()
{
    cout<<max(123,456)<<endl;
    cout<<log(exp(1))<<endl;
    
    char str[] = "Hello World";
    cout<<strlen(str)<<endl;
    cout<<str<<endl;
    
    int a[10] = {1,5,9,0,2,4,8,3,7,6};
    sort(a,a+10);        //sort函数用于排序 
    for(int i=0;i<10;i++) cout<<a[i]<<" ";
    cout<<endl;
    
    return 0;
}

关于万能头文件(#include<bits/stdc++.h>)的使用需要强调的是,在使用前需要确定编程环境或者竞赛测试环境是否允许使用(在洛谷平台可以放心使用,目前NOI系列比赛也能使用)。使用万能头文件可能会有一些潜在的隐患,例如自己使用的某个变量在万能头文件涉及的头文件中已经被定义了,那么会出现编译错误(变量命名冲突),所以要确保自己编写的程序能够在测评环境编译通过的情况下谨慎使用万能头文件,例如竞赛时进入NOI提供的官方虚拟机测试编写的程序。

函数就像一个工具,能够完成特定的功能。函数也像一个“黑盒”,按照函数的使用规则去调用函数,一般需要提供函数计算的对象(称之为参数),函数处理结束后,可能会返回一个计算结果(例如math库中的powsqrt函数),也可能没有返回值但对参数进行了处理(例如swap函数,语句swap(a,b)执行后变量a、b的值会交换,这里swap函数内部就修改了参数a、b的值)。

#include<iostream>
#include<cmath>
using namespace std;
int main()
{
	double x,y;
	cin>>x>>y;
	cout<<x<<" "<<y<<endl;
	cout<<pow(x,y)<<endl;		//调用pow函数,函数返回\(x^y\)的结果并被cout输出 
	cout<<x<<" "<<y<<endl;       //调用函数pow后,变量x,y的值不会发生变化 
	return 0;
}
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
	char str1[110],str2[110];
	cin>>str1>>str2;
	cout<<str1<<" "<<str2<<endl;
	strcpy(str1,str2);	//调用strcpy函数,参数(变量)str1会被修改成str2的值 
	cout<<str1<<" "<<str2<<endl;
	return 0;
}

调用函数时可以不关心函数具体是如何实现的,关心的是函数的功能和使用规则。也就是说函数的内部像一个“黑盒子”一样,使用者不用关心内部实现细节。而所谓函数的使用规则,就以pow函数为例,函数有两个参数,第一个参数是底数,第二个参数是指数,所以要计算\(x^y\),应该调用\(pow(x,y)\),而不是\(pow(y,x)\)。

只使用内置函数往往是不够的,解决实际问题时,很多时候需要我们根据实际情况自定义函数并调用自定义函数解决问题。

自定义函数

tangyj626阅读(438)

只使用内置函数往往是不够的,解决实际问题时,很多时候需要我们根据实际情况自定义函数并调用函数解决问题。自定义函数的优势是一次定义,需要的时候调用即可,可以有效避免大量相同或相似的代码重复出现导致程序臃肿繁琐的情况,也能让程序的逻辑结构更加清晰。

一、最简单的函数:没有参数没有返回值

我们来定义一个没有参数没有返回值的函数,函数的功能是输出"Hello World"

void say(){
	cout<<"Hello World"<<endl;
}

上面定义函数的程序代码中,void表示函数的返回值是空类型(这里表示没有返回值),函数名是say,函数名后面的小括号里没有任何内容(也就是没有任何参数),{}中的内容称为函数体。定义了函数后,在需要使用这个函数的地方直接调用函数即可:

#include<iostream>
using namespace std;

//竞赛时一般将定义函数代码写到main函数前 
void say(){
	cout<<"Hello World"<<endl;
}

int main()
{
	say();   //调用函数 
	for(int i=1;i<=10;i++) say(); 
    return 0;
} 

程序执行过程如下:

可见调用函数时,会转到函数的定义处执行函数体内容,执行完毕后返回到调用函数处。

二、有参数、没有返回值的函数

接下来,我们定义一个函数,能够输出指定行数的"Hello World"

#include<iostream>
using namespace std;

void say(int n){   //有一个int类型参数的函数 
	for(int i=1;i<=n;i++)
		cout<<"Hello World"<<endl;
}

int main()
{
	cout<<"do something"<<endl;
	say(10);   //调用函数
	cout<<"do something else"<<endl; 
    return 0;
}

定义函数代码中函数名后小括号内容int n指明函数有一个int类型的参数n,函数体中也使用了参数n(用来实现n次循环)。定义函数的代码中的参数称为形式参数,没有具体的值,只是形式上指代了一个数据。调用函数的语句say(10);,小括号中的内容称为实际参数,调用过程如下:

可知调用函数时,会转向函数定义语句处,先自动将实际参数的值赋值(拷贝)给形式参数,然后执行函数体内容,最后又返回到调用函数处。

当然可以多次调用这个函数,并且调用时可以指定不同的实际参数:

#include<iostream>
using namespace std;

void say(int n){ 
	for(int i=1;i<=n;i++)
		cout<<"Hello"<<endl;
}

int main()
{
	say(10);
	say(5); 
	say(0); 
    return 0;
} 
#include<iostream>
using namespace std;

void say(int n){ 
	for(int i=1;i<=n;i++)
		cout<<"Hello"<<endl;
}

int main()
{
	int n;
	cin>>n;
	say(n); 
    return 0;
} 

当然函数的参数也可以是多个,下面的函数用来输出\(n\)行\(m\)列由'*'组成的矩形:

#include<iostream>
using namespace std;

void print(int n,int m){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cout<<'*';
		}
		cout<<endl;
	}
}

int main()
{
	print(5,10);
	print(10,24); 
    return 0;
}

调用函数时,会按照调用语句小括号中实际参数的顺序将实际参数的值依次赋值(拷贝)给形式参数。这里甚至还可以再用一个参数指定用哪个字符组成矩形:

#include<iostream>
using namespace std;

void print(int n,int m,char c){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cout<<c;
		}
		cout<<endl;
	}
}

int main()
{
	print(5,10,'*');
	print(10,24,'@'); 
    return 0;
} 

三、有返回值的函数

前面我们调用内置函数,以math库中的数学函数为例:

cout<<pow(2,3)<<endl;
double ans = sqrt(2);
cout<<ans<<endl;
cout<<pow(2,3)+sqrt(2)<<endl;

仔细观察会发现,这里调用pow、sqrt函数与本文前面调用没有返回值的函数很不同,调用pow、sqrt函数会有一个计算结果,我们可以直接将计算结果输出,也可以赋值给变量,甚至参加运算。这样的函数是有返回值的函数,当然也可以自定义有返回值的函数。

下面我们来定义一个很简单的函数,计算两个int整数\(x\)和\(y\)的和,很显然,函数的计算结果(也就是函数的返回值)也应该是int类型:

#include<iostream>
using namespace std;

int sum(int x,int y){
	int z = x+y;
	return z;
}

int main()
{
	cout<<sum(1,2)<<endl;
	int a,b,c;
	cin>>a>>b;
	c = sum(a,b);
	cout<<c<<endl;
	cout<<sum(1,2)+sum(a,b)<<endl;
    return 0;
} 

定义函数语句开始处的int表示函数的返回值是int类型,sum是函数名,函数有两个int类型的参数x、y。函数体中int z = x+y;将x+y的结果赋值给变量z。最重要的语句是return z;表示函数返回值是变量z的值。具体执行过程如下:

调用函数时,会自动转到函数的定义语句处,将实际参数的值依次拷贝给形式参数,然后执行函数体语句。函数体中的return语句用来将该语句后的值返回到调用函数处使用。需要特别注意的是,return语句实际返回的内容应该和函数头指明的函数返回值类型一致,例如本例中return z;返回int类型变量z的值与函数头int sum(int x,int y)中指明函数返回值类型为int一致。

当然,这里还可以进一步将sum函数体中的内容精简:

int sum(int x,int y){
	return x+y;   //返回值是一个表达式(的计算结果)
}

四、main函数

现在我们应该知道,C++程序框架中的main就是我们定义的一个返回值为int类型名称为main的函数,只不过一般不会在程序中再写代码调用main函数,main函数是程序运行是被自动调用的。main函数中的return 0;表示返回 \(0\),其实main函数的返回值是可以被运行本程序的软件(例如Dev C++或者操作系统)捕获到,返回值为 \(0\) 表示程序正常结束,没有任何异常发生:

来看下面的程序:

#include<iostream>
using namespace std;
int main()
{
	cout<<5/0<<endl;    //除数是0,编译时会出现Warning,执行时会出现运行时错误
    return 0;
} 
#include<iostream>
using namespace std;
int main()
{
	int a[5];
	//数组下标溢出,导致程序出现异常
	//严格环境下这里使用a[5]就下标溢出了
	//Dev c++不是严格的运行环境,溢出很多可能都不会出现异常 
	for(int i=0;i<=1000;i++) a[i]=i;
    return 0;
} 

程序运行反馈信息如下(main返回值非 \(0\) 表示程序运行出现异常):

这就是一般情况下(特别是竞赛时)不能在main函数中return一个非 \(0\) 值的原因,这样的话判分程序会认为程序运行出现异常。

自定义函数例题

tangyj626阅读(308)

一、打印字符三角形

打印边长为\(n\)由字符\(ch\)组成的三角形。用函数实现,很显然函数没有返回值,有两个参数:int类型的参数n,表示字符三角形边长;char类型的参数ch,表示组成三角形的字符:

#include<iostream>
using namespace std;
void print(int n,char ch){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++) cout<<ch;
		cout<<endl;
	}
} 
int main()
{
	print(10,'@');
	print(5,'#');
	int m;
	char c;
	cin>>m>>c;
	//实际参数变量名和形式参数名不必相同
	print(m,c); 
    return 0;
}

需要额外注意的是,调用函数时,如果实际参数是变量,那么实际参数变量名不必和函数的形式参数相同。

如果不使用函数,可以预见程序中会重复出现相同或相似的代码,让整个程序显得臃肿繁琐:

#include<iostream>
using namespace std;
int main()
{
	for(int i=1;i<=10;i++){
		for(int j=1;j<=i;j++) cout<<'@';
		cout<<endl;
	}
	
	for(int i=1;i<=5;i++){
		for(int j=1;j<=i;j++) cout<<'#';
		cout<<endl;
	}
	
	int m;
	char c;
	cin>>m>>c;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=i;j++) cout<<c;
		cout<<endl;
	}
    return 0;
}

如果我们将print函数定义语句放到main函数的后面,会发生编译错误:

#include<iostream>
using namespace std;
int main()
{
    print(10,'@');    //会发生编译错误
    return 0;
}
void print(int n,char ch){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++) cout<<ch;
        cout<<endl;
    }
} 

其实我们可以将自定义函数定义语句放到main函数的后面,此时需要在main函数前申明自定义函数,最简单的做法就是将自定义函数的函数头拷贝到main函数前:

#include<iostream>
using namespace std;
void print(int n,char ch);   //申明函数(注意语句结尾处的分号) 
int main()
{
    print(10,'@'); 
    return 0;
}
void print(int n,char ch){
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++) cout<<ch;
        cout<<endl;
    }
} 

二、计算最大值

计算两个int整数\(x\)和\(y\)的最大值。用函数实现,很显然函数的返回值类型应该设置为int类型,有两个int类型的参数:

#include<iostream>
using namespace std;

int max(int x,int y){
	if(x>y) return x;
	else return y;
}

int main()
{
	cout<<max(1,2)<<endl;
	int a,b;
	cin>>a>>b;
	cout<<max(a,b)<<endl;
    return 0;
}

这里max函数还可以进一步精简:

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

函数体中只要运行到return语句就强制结束函数返回到调用处,不会再执行函数体return语句后面的内容。

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

这里使用了条件运算符来精简代码。

三、大写字母转小写

将大写字母转换成对应的小写字母。用函数实现,返回值类型为char,参数类型也是char:

#include<iostream>
using namespace std;

char convert(char ch){
	if(ch>='A' && ch<='Z') return ch+32;
	return ch;
}

int main()
{
	cout<<convert('D')<<endl;
	cout<<convert('$')<<endl;
	char c;
	cin>>c;
	cout<<convert(c)<<endl;
    return 0;
} 

四、判断闰年

判断正整数\(y\)对应年份是否为闰年。用函数实现,可以将函数的返回值设置为bool类型:

#include<iostream>
using namespace std;

bool leap(int y){
	if((y%4==0 && y%100!=0) || y%400==0) return true;
	return false;
}

int main()
{
	int n;
	cin>>n;
	if(leap(n)==true) cout<<"Leap year"<<endl;
	else cout<<"Nonleap year"<<endl;
    return 0;
} 
#include<iostream>
using namespace std;

bool leap(int y){
	return (y%4==0 && y%100!=0) || y%400==0;  //直接返回判断结果
}

int main()
{
	int n;
	cin>>n;
	if(leap(n)) cout<<"Leap year"<<endl;      //==true可以省略
	else cout<<"Nonleap year"<<endl;
    return 0;
} 

五、判断素数

判断正整数\(n\)是否为素数。用函数实现,可以将函数的返回值设置为bool类型:

#include<iostream>
using namespace std;

//判断n是否为素数 
bool isPrime(int n){
	if(n==1) return false;
	
	for(int i=2;i*i<=n;i++){
		if(n%i==0) return false;
	}
	
	return true;
} 

int main()
{
    int m;
    cin>>m;
    if(isPrime(m)) cout<<"YES";
	else cout<<"NO";
    return 0;
}

比较这里的函数和之前不用函数的语句,会发现函数体中巧妙使用return语句强制退出函数可以让程序结构更加精简:

bool isPrime(int n){
	if(n==1) return false;	
	for(int i=2;i*i<=n;i++){
		if(n%i==0) return false;
	}	
	return true;
}
//调用函数
int n;
cin>>n;
if(isPrime(n)){
}
int n;
bool f = true; //标记变量
cin>>n;
if(n==1) f = false;
for(int i=2;i*i<=n;i++){
	if(n&i==0){
		f = false;
		break;
	}
}
if(f){
}

六、计算三角形周长和面积

问题:输入三角形三个顶点的坐标,计算并输出三角形的周长和面积。

#include<iostream>
#include<cmath>
using namespace std; 
//计算平方 
double sq(double x){
	return x*x;
}

//计算两坐标点距离(线段长度)
double dist(double x1,double y1,double x2,double y2){
	return sqrt(sq(x1-x2)+sq(y1-y2));
}

//计算三角形周长
double len(double a,double b,double c){
	return a+b+c;
}

//计算三角形面积
double area(double a,double b,double c){
	double p = len(a,b,c)/2;
	return sqrt(p*(p-a)*(p-b)*(p-c));
}
int main()
{
    double x1,y1,x2,y2,x3,y3;
	cin>>x1>>y1>>x2>>y2>>x3>>y3;
	double a = dist(x1,y1,x2,y2);
	double b = dist(x2,y2,x3,y3);
	double c = dist(x3,y3,x1,y1);
	cout<<len(a,b,c)<<" "<<area(a,b,c)<<endl;
    return 0;
}

很显然,可以在自定义函数中调用其他自定义函数。通过上面的程序代码可以看出,借助自定义函数,main函数的结构变得如此简单清晰,可读性大大提高,这也是使用函数的优势。

局部变量与全局变量

tangyj626阅读(201)

我们来看前面累加求和和判断素数的程序代码:

#include<iostream>
using namespace std; 
int main()
{
    int s = 0;
    for(int i=1;i<=100;i++){
    	s += i;
	}
	cout<<s<<endl;
    return 0;
}

↑控制变量i申明语句放在了for循环语句中↑

#include<iostream>
using namespace std; 
int main()
{
    int n,i;
    cin>>n;
    for(i=2;i*i<=n;i++){
    	if(n%i==0) break;
	}
	if(i*i>n) cout<<"YES";
	else cout<<"NO";
    return 0;
}

↑控制变量i申明语句放在了for循环语句前↑

判断素数的程序,在for循环结束后判断循环的条件不成立(说明循环正常结束,没有被break掉)来判断是素数。循环结束后的if语句的条件中使用了前面循环的控制变量,这个时候就不能将循环控制变量的申明语句放在循环语句中:

#include<iostream>
using namespace std; 
int main()
{
    int n;
    cin>>n;
    for(int i=2;i*i<=n;i++){   //i是局部变量,只能在循环语句中使用
    	if(n%i==0) break;
	}
	if(i*i>n) cout<<"YES"; 	//出现编译错误
	else cout<<"NO";
    return 0;
}

上面的程序会出现编译错误,原因是for(int i=2;i*i<=n;i++)方式申明的变量i只能在这个for循环中使用,出了循环后就不能再使用了,这样的变量称为局部变量,局部变量能够使用的范围称为局部变量的作用域

同样地,自定义函数的参数和函数体中申明的变量也都是局部变量,只能在函数体中使用。甚至可以简单地认为{}中申明的变量就是局部变量,只能在{}范围内使用,也就是局部变量的作用域是包含局部变量定义语句的最内层花括号{}(即使省略了花括号),并且从定义局部变量语句后算起。

#include<iostream>
using namespace std;
int count(int n){	//参数n是count函数的局部变量,只能在count函数中使用 
	int cnt = 0;	//cnt是count函数的局部变量,只能在count函数中使用 
	for(int i=1;i<=n;i++){	//i是for循环的局部变量,只能在for循环中使用 
		if(i%5==3 && i%7==6) cnt++;
	}
	//如果在这里使用变量i会出现编译错误 
	return cnt;
} 
int main()
{ 
    int n;	//n是main函数的局部变量,只能在main函数内使用 
    cin>>n;
    cout<<count(n)<<endl;
    //cout<<cnt<<endl;  //会出现编译错误
    return 0;
}

来看下面一段有意思的程序:

#include<iostream>
using namespace std;
int main()
{ 
    int n = 123;
    for(int i=1;i<=5;i++){
    	cout<<n<<endl;	//此时n是for循环外的变量n 
    	int n = 0;		//在for循环体中申明局部变量n(与循环体外变量n同名) 
    	cout<<n<<endl;	//此时n是for循环体中申明的变量n
    	//申明同名变量n后for循环体中无法使用循环外的变量n
		//也就是for循环外的同名变量n被“屏蔽”掉了 
	}
	cout<<n<<endl;		//此时n是for循环外的变量n 
    return 0;
}

前面我们已经接触到全局变量的使用——开大数组的时候将数组的申明语句放到main函数的前面,这个时候的数组就是全局数组。我们也可以将普通变量定义到函数外部,这个时候的变量就是全局变量。来看下面的程序:

#include<iostream>
using namespace std;
int n;	//全局变量 
void test(){
	cout<<"test函数中修改n值前,n="<<n<<endl;
	n++;
	cout<<"test函数中修改n值后,n="<<n<<endl;
}
int main()
{
	cout<<"执行输入语句前,n="<<n<<endl;
	cin>>n;
	cout<<"main函数中n="<<n<<endl;
	test();
	cout<<"main函数中n="<<n<<endl;
	n = 0;
	test();
	cout<<"main函数中n="<<n<<endl;
    return 0;
}

运行程序可知,全局变量在main函数和test函数中都可以使用,并且在任意地方修改了全局变量的值,在其它地方获取到的是修改后的新值。

全局变量还有一个特点,就是其值会被自动初始化,例如数值型(int、long long、float、double等)全局变量被自动初始化为 \(0\),字符型全局变量被自动初始化为'\0'(其实就是十进制 \(0\)),bool类型全局变量被自动初始化为false。同样的,全局数组所有的元素同样也会被自动初始化。

函数参数传递

tangyj626阅读(254)

一、值传递

来看一个例子,使用函数交换两个变量的值:

#include<iostream>
using namespace std; 
void swap(int x,int y){
	int t = x;
	x = y;
	y = t;
}
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<a<<" "<<b<<endl;
    swap(a,b);
    cout<<a<<" "<<b<<endl;
    return 0;
}

运行程序会发现,调用swap(a,b)并不能交换a、b两个变量的值!原因很简单,执行swap函数时,会将实际参数a、b的值赋值(拷贝)给swap函数的形式参数x、y,而 x、y是swap函数的局部变量,a、b是main函数的局部变量,在内存里a与x,b与y不是同一个存储单元,swap函数能够交换x、y的值,但并不能影响到main函数中调用swap函数的实际参数a、b的值。这样的将实际参数的值赋值(拷贝)给函数的形式参数的传递方式称为值传递。值传递的函数体中修改形式参数的值不会影响到实际参数的值。

二、引用传递

我们将swap函数作一点小小的改动:

#include<iostream>
using namespace std; 
void swap(int &x,int &y){
	int t = x;
	x = y;
	y = t;
}
int main()
{
    int a,b;
    cin>>a>>b;
    cout<<a<<" "<<b<<endl;
    swap(a,b);
    cout<<a<<" "<<b<<endl;
    return 0;
}

其实就是在形式参数x、y前添加了一个特殊的符号&,表示引用传参,这个时候形式参数x和实际参数a是同一个存储单元(x相当于a的别名),形式参数y与实际参数b也是同样的关系,那么在函数体中修改形式参数x、y的值,就会影响到实际参数a、b的值。

可以利用引用传递的特点,设计函数来直接改变实际参数的值,例如下面的将字符串转大写的程序,大家仔细阅读并对比两段程序代码:

#include<iostream>
#include<string>
using namespace std;
//值传递参数
//函数返回值为string,返回转大写后的字符串
string convert(string s){
	//修改参数s中的小写字母为大写
	for(size_t i=0;i<s.length();i++){
		//元素为小写字母,改为对应大写字母
		if(s[i]>='a' && s[i]<='z')
			s[i] -= 32;
	}
	return s;  //返回修改后的字符串
}

int main()
{
	string ss = "Hello World";
	cout<<ss<<endl;
	cout<<convert(ss)<<endl;
    return 0;
}
#include<iostream>
#include<string>
using namespace std;
//引用传递参数
//在函数体中修改引用参数字符串中的小写字母为大写
void convert(string &s){
	for(size_t i=0;i<s.length();i++){
		//元素为小写字母,改为对应大写字母
		if(s[i]>='a' && s[i]<='z')
			s[i] -= 32;
	}
}

int main()
{
	string ss = "Hello World";
	cout<<ss<<endl;
	convert(ss);
	cout<<ss<<endl;
    return 0;
}

三、地址传递

另外还要注意到函数参数是数组的情况,看下面的程序:

#include<iostream>
using namespace std; 
void test(int a[]){    //参数是数组
	a[0]++;
}
int main()
{
    int s[5] = {1,2,3,4,5};
    cout<<s[0]<<endl;
    test(s);
    cout<<s[0]<<endl;
    return 0;
}

运行程序会发现,执行test(s)后,s[0]的值发生了变化,也就是test函数中修改a[0]的值影响到了s[0]。其实,如果函数的参数是数组,形式参数和实际参数其实是同一个数组,数组形式参数就是数组实际参数的别名,在函数体中修改数组形式参数的元素当然会影响数组实际参数的元素。这样的实际参数传递到形式参数的方式称为地址传递

再来试试下面的程序,进一步体会数组作为函数参数时,地址传递的作用机制:

#include<iostream>
using namespace std;
int a[1001];
//数组作为参数,一般还需要一个参数来说明数组实际存储元素的个数
void read(int arr[],int n){
	for(int i=0;i<n;i++)
		cin>>arr[i];
}
void exec(int arr[],int n){
	for(int i=0;i<n;i++)
		arr[i] *= arr[i];
}
void out(int arr[],int n){
	for(int i=0;i<n;i++)
		cout<<arr[i]<<" ";
	cout<<endl;
}
int main()
{
    int n;
    cin>>n;
    read(a,n);
    out(a,n);
    exec(a,n);
    out(a,n);
    return 0;
}
#include<iostream>
using namespace std;
int n,a[1001];
//自定义函数中使用全局变量 
void read(){
	for(int i=0;i<n;i++)
		cin>>a[i];
}
void exec(){
	for(int i=0;i<n;i++)
		a[i] *= a[i];
}
void out(){
	for(int i=0;i<n;i++)
		cout<<a[i]<<" ";
	cout<<endl;
}
int main()
{
    cin>>n;
    read();
    out();
    exec();
    out();
    return 0;
}

其实我们也可以自定义函数实现求字符串长度或者字符串所有字符转换成小写:

#include<iostream>
using namespace std;
int strlen(char str[]){
	int n = 0;
	for(int i=0;str[i];i++) n++;
	return n;
}
void strlwr(char str[]){
	for(int i=0;str[i];i++){
		if(str[i]>='A' && str[i]<='Z') str[i] += 'a'-'A';	
	}
}
int main()
{
	cout<<strlen("")<<endl;
	char str[] = "Hello World!";
	strlwr(str);
	cout<<str<<endl;
    return 0;
}

函数使用地址传参更普遍的方式是使用指针作为函数的参数,具体的方法可以查阅这篇文章:指针

快读

tangyj626阅读(166)

遇到要输入处理较多整数的题目时,用scanf或者cin肯定是不够快的。由于getchar()有速度快的特性,所以可以使用getchar()整数按照单个字符的方式输入并重新组装,对比scanf,这样做效率可以提高数倍。可以将以“getchar()读入整数每一位并组装”的过程写成一个函数以方便调用。

int read(){
	//f记录符号 
	int s = 0, f = 1;
	char ch = getchar();
	//读入符号(过滤了非数字字符) 
	while (ch < '0' || ch > '9'){
		if (ch == '-') f = -1;
		ch = getchar();
	}
	//读数字字符并组装整数 
	while (ch >= '0' && ch <= '9'){
		s = s * 10 + ch - '0';
		ch = getchar();
	}
	return s * f;
}
//调用方法
// int num = read();
void read(int &n){
	//f记录符号 
	int s = 0, f = 1;
	char ch = getchar();
	//读入符号(过滤了非数字字符) 
	while (ch < '0' || ch > '9'){
		if (ch == '-') f = -1;
		ch = getchar();
	}
	//读数字字符并组装整数 
	while (ch >= '0' && ch <= '9'){
		s = s * 10 + ch - '0';
		ch = getchar();
	}
	n = s * f;
}
//调用方法
// int num;
// read(num);

其中的组装十进制整数的语句可以借助位运算实现(能提高一些效率):

s = (s<<3)+(s<<1)+ch-'0';   //s<<3 s左移三位相等于 s*8;s<<1 s左移一位相等于 s*2,移位运算效率更高

如果读入的是非负整数,那么函数还可以进一步简化:

int read(){
	int s = 0;
	char ch = getchar();
	//读数字字符并组装整数 
	while (ch >= '0' && ch <= '9'){
		s = (s<<3) + (s<<1) + ch - '0';
		ch = getchar();
	}
	return s;
}
//调用方法
// int num = read();
void read(int &s){
	s = 0;
	char ch = getchar();
	//读数字字符并组装整数 
	while (ch >= '0' && ch <= '9'){
		s = (s<<3) + (s<<1) + ch - '0';
		ch = getchar();
	}
}
//调用方法
// int num;
// read(num);

如果读入的是 \(p\) (\(2 \leq p \leq 16\))进制的非负整数(读入后转成十进制数),函数可以这样设计:

int read(int p){
	int s = 0;
	char ch = getchar();
	//读字符并组装整数 
	while (true){
		if(ch >= '0' && ch <= '9')
			s = s * p + ch - '0';
		else if(ch >= 'a' && ch <= 'f')
			s = s * p + ch - 'a' + 10;
		else if(ch >= 'A' && ch <= 'F')
			s = s * p + ch - 'A' + 10;
		else break;
		ch = getchar();
	}
	return s;
}
//调用方法
// int num = read(16);
void read(int &s,int p){
	s = 0;
	char ch = getchar();
	//读字符并组装整数 
	while (true){
		if(ch >= '0' && ch <= '9')
			s = s * p + ch - '0';
		else if(ch >= 'a' && ch <= 'f')
			s = s * p + ch - 'a' + 10;
		else if(ch >= 'A' && ch <= 'F')
			s = s * p + ch - 'A' + 10;
		else break;
		ch = getchar();
	}
}
//调用方法
// int num;
// read(num,16);

通过一个问题来看快读函数的调用,先输入正整数n,然后输入n组数据,每组数据一行包括三个用一个空格隔开的非负整数,计算并输出每组整数的和。

#include<cstdio> 
int read(){
    int s = 0;
    char ch = getchar();
    //读数字字符并组装整数 
    while (ch >= '0' && ch <= '9'){
        s = (s<<3) + (s<<1) + ch - '0';
        ch = getchar();
    }
    return s;
}
int main()
{
	int n,a,b,c;
	n = read();
	for(int i=1;i<=n;i++){
		a = read();
		b = read();
		c = read();
		printf("%d\n",a+b+c);
	}
	return 0;
}
#include<cstdio> 
void read(int &s){
	s = 0;
	char ch = getchar();
	//读数字字符并组装整数 
	while (ch >= '0' && ch <= '9'){
		s = (s<<3) + (s<<1) + ch - '0';
		ch = getchar();
	}
}
int main()
{
	int n,a,b,c;
	read(n);
	for(int i=1;i<=n;i++){
		read(a);
		read(b);
		read(c);
		printf("%d\n",a+b+c);
	}
	return 0;
}

递归函数

tangyj626阅读(333)

函数体中可以调用其它函数,如果一个函数题中又调用了自己,这样的函数称为递归函数。显然,函数体中不应该是无条件的递归调用,否则就和“死循环”一样的效果了。也就是在满足某些条件的时候不递归调用自身,这也就是递归函数的“出口”。

一、计算阶乘

对于阶乘,\(n! = 1 \times 2 \times ... \times (n-1) \times n\),\((n-1)! = 1 \times 2 \times ... \times (n-1) \),可知:

\(\begin{equation} n!=\left\{ \begin{aligned} 1\quad(n=1) \\ n \times (n-1)! \quad(n>1)\end{aligned} \right. \end{equation}\)

如果用\(f(n)\)表示\(n!\),可知:

\(\begin{equation} f(n)=\left\{ \begin{aligned} 1\quad(n=1) \\ n \times f(n-1) \quad(n>1)\end{aligned} \right. \end{equation}\)

如果用自定义函数int f(int n);来计算并返回\(n!\)的值,那么在函数体里可以使用n*f(n-1)来作为计算结果,可见函数int f(int n);中又调用了自己,这就是递归函数:

#include<iostream>
using namespace std;
int f(int n){
	if(n==1) return 1;	//递归出口 
	return n*f(n-1);	  //递归调用自身 
}
int main()
{
	for(int i=1;i<=10;i++){
		cout<<f(i)<<endl;
	}
    return 0;
}

以main函数中i=3时,调用f(3)为例,整个调用过程如下:

从上面分析可知,递归函数有点类似于“剥洋葱”,一层套着一层,要向下一直“剥”到最里层(也就是递归出口),然后还有向上逐层返回的过程。

二、逆序输出

再来看一个问题,输入\(n\)个整数,然后逆序输出所有的整数,我们知道在输入一个整数后需要将这个整数保存下来(当然要保存所有整数需要用到数组),等所有整数都输入完毕了,才能逆序输出:

#include<iostream>
using namespace std;
int a[1011]; 
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=n;i>=1;i--) cout<<a[i]<<" ";
    return 0;
}

其实也可以使用递归函数来实现,大家可以自行分析下面参考程序递归函数的执行过程:

#include<iostream>
using namespace std;
int n;		//n是全局变量 
//处理第k个数:输入并输出
void f(int k){
    if(k==n+1) return;	//递归出口
    int m;
    cin>>m;
    f(k+1);		//递归调用,处理第n+1个数
    cout<<m<<" ";
}
int main(){
    cin>>n;
    f(1);
    return 0;
}
#include<iostream>
using namespace std;
//输入并输出
void f(int k){
	if(k==0) return;  //递归出口
	int m;
	cin>>m;
	f(k-1);		//递归调用
	cout<<m<<" ";
}
int main(){
    int n;
    cin>>n;
    f(n);
    return 0;
}

三、斐波那契数列

前面介绍的斐波那契数列的第\(n\)项\(fib(n)\)的计算公式是:

\(\begin{equation} fib(n)=\left\{ \begin{aligned} 1\quad(n \le 2) \\ fib(n-1)+fib(n-2) \quad(n \ge 3)\end{aligned} \right. \end{equation}\)

和上面的计算阶乘一样,可以使用递归函数处理,参考程序如下:

#include<iostream>
using namespace std;
long long fib(int n){
	if(n<=2) return 1;			//递归出口 
	return fib(n-1)+fib(n-2);	 //递归调用自身(2次) 
}
int main()
{
	cout<<fib(20)<<endl;
    return 0;
}

如果调用fib(40),会发现耗时已经接近1s了,如果调用fib(50),耗时远远超过1s(实际测试在普通配置PC上耗时40s左右),这是什么原因呢?我们修改一下代码:

#include<iostream>
using namespace std;
long long fib(int n){
	cout<<"fib("<<n<<")"<<endl;	//通过输出额外信息来测试函数调用过程 
	if(n<=2) return 1;			 //递归出口 
	return fib(n-1)+fib(n-2);	  //递归调用自身 
}
int main()
{
	cout<<fib(6)<<endl;
    return 0;
}

在fib函数里添加上语句cout<<"fib("<<n<<")"<<endl;,那么每调用一次fib函数,就会输出fib函数“调用”信息。程序输出结果如下:

fib(6)
fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(2)
fib(3)
fib(2)
fib(1)
fib(4)
fib(3)
fib(2)
fib(1)
fib(2)
8
fib(6)调用过程

可知调用fib(6),一共调用了2次fib(4)、3次fib(3)、5次fib(2)、3次fib(1),其实这些调用一次就已经计算出结果了,这里出现了不必要的重复调用的情况。如果调用fib(50),仅fib(20)就被重复调用多达1346269次,fib(4)更是被调用了2971215073次(接近30亿次)。这么多不必要的重复调用导致程序效率低下。

那么如何提高程序的运行效率呢?可以用一个全局数组a来记录计算结果,在函数体中,将fib(n)的结果保存到a[n]中,在计算前判断a[n]的值不为0的话可以直接返回a[n](已经计算过):

#include<iostream>
using namespace std;
long long a[51];  //全局数组用来记录fib数列每项 
long long fib(int n){
	if(a[n]) return a[n]; //已经计算过,直接返回 
	if(n<=2) return a[n] = 1;
	return a[n] = fib(n-1)+fib(n-2); 
}
int main()
{
	cout<<fib(50)<<endl;
    return 0;
}

四、辗转相除法求最大公约数

再来看辗转相除法求最大公约数,前面已经介绍了该算法的思路:

  1. 输入m和n
  2. r←m%n,若r==0,输出n,算法结束。
  3. 互换:执行赋值,m←n,n←r,并返回第2步。

可以使用do...while循环求解问题:

#include<iostream>
using namespace std;
int main()
{
    int m,n,r;
    cin>>m>>n;    
    do{
        r = m%n;
        m = n;
        n = r;
    }while(r!=0);
    cout<<m;   //注意这里是输出m 
    return 0;
} 

分析上面的第 3 步,其实就是计算 n 和 r(也就是 m%n)的最大公约数。可以用递归函数实现:

\(\begin{equation} gcd(m,n)=\left\{ \begin{aligned} n\quad(m\%n=0) \\ gcd(n,m\%n) \quad(m\%n \neq 0)\end{aligned} \right. \end{equation}\)

还可以简化为:

\(\begin{equation} gcd(m,n)=\left\{ \begin{aligned} m\quad(n=0) \\ gcd(n,m\%n) \quad(n>1)\end{aligned} \right. \end{equation}\)

#include<iostream>
using namespace std;
int gcd(int m,int n){
	return m%n==0?n:gcd(n,m%n);
}
int main()
{
    int x,y;
    cin>>x>>y;
    cout<<gcd(x,y)<<endl;
    return 0;
}
#include<iostream>
using namespace std;
int gcd(int m,int n){
	return n==0?m:gcd(n,m%n);
}
int main()
{
    int x,y;
    cin>>x>>y;
    cout<<gcd(x,y)<<endl;
    return 0;
}

求最大公约数还有辗转相减法:

\(\begin{equation} gcd(m,n)=\left\{ \begin{aligned} m\quad(m=n) \\ gcd(n,m) \quad(m<n)\\ gcd(n,m-n) \quad(m>n)\end{aligned} \right. \end{equation}\)

还可以对辗转相减法改良,用来高效计算高精度数(后面会介绍)的最大公约数:

思考问题

1.输入正整数\(m,n(m\leq3,n\leq5)\),计算Ackmann函数的结果:

\(\begin{equation} akm(m,n)=\left\{ \begin{aligned} n+1\quad(m=0) \\ akm(m-1,1) \quad(m>0且n=0) \\ akm(m-1,akm(m,n-1)) \quad(m>0且n>0)\end{aligned} \right. \end{equation}\)

2.\(n\)条两两相交的直线将平面划分成多少个区域?注意没有任何超过2条直线相交于同一点的情况。

3.楼梯有\(n\)级台阶,上楼可以一步上一级台阶,也可以一步上两级台阶。编一程序,计算共有多少种不同的走法。

结构体

tangyj626阅读(582)

结构体能够将一些不同类型的信息“捆绑”聚合成一个整体,例如可以用一个结构体来记录学生的姓名(字符串类型)、性别(char类型)、年龄(int类型)等信息。结构体就像int、double一样,是一种(自定义的)数据类型。

一、结构体的基本使用方法

先来看一个问题:P5740 【深基7.例9】最厉害的学生

分析:使用“打擂台”方式求极值,只不过这里要记录下最高分的学生的姓名和三科成绩:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string name,ans_name;
    int n,chinese,math,english,total;
	int ans_chinese,ans_math,ans_english,ans_total;
    ans_total = 0;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>name>>chinese>>math>>english;
        total = chinese+math+english;
        if(total>ans_total){
            ans_total = total;
            ans_name= name;
            ans_chinese = chinese;
            ans_math = math;
            ans_english = english;
        }
    }
    cout<<ans_name<<" "<<ans_chinese<<" "<<ans_math<<" "<<ans_english<<endl;
    return 0;
}

上面的程序使用了5个变量来存储每次输入的学生姓名、语文、数学、英语和总分5项信息,同样用5个变量记录了最高分学生的5项信息,“打擂”过程中,遇到新的最高分,记录最高分学生的5项信息都要更新,这样操作有点繁琐。

可以使用结构体将学生的5项信息“捆绑打包”聚合成一个自定义数据类型:

#include<iostream>
#include<string>
using namespace std;
//定义一个结构体Student来记录学生信息,Student是一个自定义数据类型
//结构体中name、chinese等是Student的成员变量(书写就像定义普通变量一样)
struct Student{
    string name;
    int chinese,math,english,total;
};
int main()
{
    //定义Student类型变量one和ans
    //one用来记录每次输入的学生信息,ans记录最高分学生信息
    Student one,ans; 

    ans.total = 0;
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        //输入到one的name、chinese、math、english成员变量中 
        cin>>one.name>>one.chinese>>one.math>>one.english;
        //计算one的total成员变量 
        one.total = one.chinese+one.math+one.english;
        
        if(one.total>ans.total){
            ans = one;    //非常简单的直接赋值来记录最高分学生信息 
        }
    }
    cout<<ans.name<<" "<<ans.chinese<<" "<<ans.math<<" "<<ans.english<<endl;
    return 0;
}

结构体是由一系列数据(可以是不同的数据类型)构成的数据集合,并且结构体提供了一种自定义数据类型的手段。定义结构体的一般格式如下:

struct 类型名称{
    数据类型1 成员变量1(或者成员变量列表);
    数据类型2 成员变量2(或者成员变量列表);
    //...其它成员变量
};

结构体定义的是一种数据类型,所以可以像申明int、double、string类型的变量那样来申明结构体变量。此外,可以用结构体变量名.成员名来使用结构体内部的成员变量,这些成员变量的使用方法和普通的变量一致,可以输入、输出、赋值、参加运算(上面例子中cin>>one.xm>>one.yw>>one.sx>>one.yy;one.zf = one.yw+one.sx+one.yy;one.zf>ans.zf)。更神奇的是,相同类型的结构体变量之间可以直接赋值(上面例子中更新最高分学生信息时直接使用ans = one;)。

其实结构体中的成员变量也可以是结构体类型,例如上面的例子可以将成绩信息定义为一个Score结构体类型,学生信息定义为一个Student结构体类型,Student结构体中用一个Score结构体类型成员变量记录所有成绩信息:

#include<iostream>
#include<string>
using namespace std;
struct Score{
    int chinese,math,english,total;
};
struct Student{
    string name;
    Score score;
};
int main()
{
    Student one,ans; 

    ans.score.total = 0;
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>one.name>>one.score.chinese>>one.score.math>>one.score.english;
        one.score.total = one.score.chinese+one.score.math+one.score.english;
        
        if(one.score.total>ans.score.total){
            ans = one; 
        }
    }
    cout<<ans.name<<" "<<ans.score.chinese<<" "<<ans.score.math<<" "<<ans.score.english<<endl;
    return 0;
}

二、结构体数组

问题:P5741 旗鼓相当的对手 - 加强版

分析:用结构体Student存储一位学生信息,用结构体Student数组a来存储所有学生信息。通过循环列举1~N-1的每位学生a[i],对于a[i]判断其与i+1~N的每位学生a[j]是否是“旗鼓相当的对手”即可(这样不会出现重复判断):

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english,total;
};
Student a[1001];

bool judge(int x,int y,int z){  //判断|x-y|<=z? 
    return x-y<=z && y-x<=z;
}
bool check(Student a,Student b){ //判断a、b两位学生是否是“旗鼓相当的对手” 
    return judge(a.chinese,b.chinese,5) && judge(a.math,b.math,5) && judge(a.english,b.english,5) && judge(a.total,b.total,10);
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i].name>>a[i].chinese>>a[i].math>>a[i].english;
        a[i].total = a[i].chinese+a[i].math+a[i].english;
    }
    for(int i=1;i<n;i++){
        //从第i+1位开始的每位学生a[j],判断a[i]与a[j] 
        for(int j=i+1;j<=n;j++){
            if(check(a[i],a[j])){
                cout<<a[i].name<<" "<<a[j].name<<endl;
            }
        }
    }
    return 0;
}

三、*结构体成员函数

结构体中除了成员变量外,还可以有成员函数,成员函数类似于函数,可以认为它是结构体的“内部函数”,看下面的例子:

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english;
    //成员函数(输出学生姓名和三科成绩)
    void print(){
    	cout<<name<<" "<<chinese<<" "<<math<<" "<<english<<endl;
	}
	//有返回值的成员函数(计算三科成绩总分)
	int total(){
		return chinese+math+english;
	}
};
int main()
{
    Student one;
    one.name = "Jerry";
	one.chinese = 120;
	one.math = 138;
	one.english = 145;
	one.print();    //调用成员函数
	cout<<one.total()<<endl;  //调用成员函数(有返回值)
    return 0;
}

结构体有一种特殊的成员函数,其函数名与结构体名称相同,称为构造函数,可以用来初始化结构体:

struct Student{
    string name;
    int chinese,math,english;
    Student(){}   //没有任何参数的(默认)构造函数
    //带参数的构造函数(参数名和成员名不同,特意加了一个_)
    Student(string _name,int _chinese,int _math,int _english){
    	this->name = _name;    //可以简写成 name = _name; name是结构体成员,_name是参数
    	this->chinese = _chinese;
    	this->math = _math;
    	this->english = _english;
	}
};

//注意比较和上面定义结构体的不同(带参构造函数的名称)
struct Student{
    string name;
    int chinese,math,english;
    Student(){}   //没有任何参数的(默认)构造函数
    //带参数的构造函数(参数名和成员名相同) 
    Student(string name,int chinese,int math,int english){
    	this->name = name;    //不能简写成 name = name;因为参数(局部变量)name将同名的成员name屏蔽了
    	this->chinese = chinese;
    	this->math = math;
    	this->english = english;
	}
};

上面代码定义的结构体Student中有两个构造函数,一个是没有任何参数的Student();另外一个是有参数的Student(string _name,int _chinese,int _math,int _english);有了构造函数,就可以用构造函数来初始化结构体:

//原始的初始化结构体变量方法 
Student s;
s.name= "Jerry";
s.chinese = 120;
s.math  = 135;
s.english = 150; 

//借助结构体构造函数初始化后赋值给结构体变量
Student ss = Student("Jerry",120,135,150);

//使用结构体的构造函数直接初始化结构体变量 
Student sss("Jerry",120,135,150);

下面来看成员函数的示例程序:

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english;
    Student(){}   //没有任何参数的(默认)构造函数 
    Student(string _name,int _chinese,int _math,int _english){
    	this->name = _name;    //可以简写成 name = _name;
    	this->chinese = _chinese;
    	this->math = _math;
    	this->english = _english;
	}
	int total(){
		return chinese+math+english;
	}
	void print(){
		cout<<name<<" "<<chinese<<" "<<math<<" "<<english<<endl;
	}
};

int main()
{	
	//使用结构体的构造函数初始化变量 
	Student s("Jerry",120,135,150);
    cout<<s.total()<<endl;    //调用结构体成员方法
    s.print();                //调用结构体成员方法
    return 0;
}

通过上面的例子,我们应该能够体会到前面使用string类型变量的length()方法获取字符串长度(string s;cin>>s;cout<<s.length();),其实string是一个类(和结构体相似,但功能比结构体更强),length是其中的成员函数(又称为方法)。

C++还支持更简洁的结构体初始化语句:

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english;
};

int main()
{	
	//更简洁的结构体初始化语句 
	Student s{"Jerry",120,135,150};
    cout<<s.name<<" "<<s.chinese<<" "<<s.math<<" "<<s.english<<endl;
    return 0;
}

借助成员方法,上一个问题“P5741 旗鼓相当的对手 - 加强版”还可以如下求解:

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english;
    Student(){}   //没有任何参数的(默认)构造函数 
    Student(string _name,int _chinese,int _math,int _english){
        this->name = _name;    //可以简写成 name = _name;
        this->chinese = _chinese;
        this->math = _math;
        this->english = _english;
    }
    int total(){   //计算返回总分
    	return chinese+math+english;
	}
    bool judge(int x,int y,int z){  //判断|x-y|<=z? 
        return x-y<=z && y-x<=z;
    }
    bool check(Student a) {  //判断当前学生与学生a是否是“旗鼓相当的对手” 
        return judge(chinese,a.chinese,5) && judge(math,a.math,5) && judge(english,a.english,5) && judge(total(),a.total(),10);
    }
};
Student a[1001];

int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
    	string name;
    	int chinese,math,english;
        cin>>name>>chinese>>math>>english;
        a[i] = Student(name,chinese,math,english);
    }
    for(int i=1;i<n;i++){
        //从第i+1位开始的每位学生a[j],判断a[i]与a[j] 
        for(int j=i+1;j<=n;j++){
            if(a[i].check(a[j])){
                cout<<a[i].name<<" "<<a[j].name<<endl;
            }
        }
    }
    return 0;
}

四、*重载输入输出运算符输入输出结构体变量

上面的程序输入并存储到结构体变量的语句仍然比较繁琐,可以重载输入运算符实现直接输入成员数据存储到结构体变量中,同样地还可以重载输出运算符实现直接输出结构体变量。

#include<iostream>
#include<string>
using namespace std;
struct Student{
    string name;
    int chinese,math,english;
    Student(){}   //没有任何参数的(默认)构造函数 
    Student(string _name,int _chinese,int _math,int _english){
        this->name = _name;    //可以简写成 name = _name;
        this->chinese = _chinese;
        this->math = _math;
        this->english = _english;
    }
    int total(){   //计算返回总分
        return chinese+math+english;
    }
    bool judge(int x,int y,int z){  //判断|x-y|<=z? 
        return x-y<=z && y-x<=z;
    }
    bool check(Student a) {  //判断当前学生与学生a是否是“旗鼓相当的对手” 
        return judge(chinese,a.chinese,5) && judge(math,a.math,5) && judge(english,a.english,5) && judge(total(),a.total(),10);
    }
};
Student a[1001];
//重载输入运算符
istream &operator>>(istream &in, Student &s){
    in>>s.name>>s.chinese>>s.math>>s.english;
    return in;
}
//重载输出运算符
ostream &operator<<(ostream &os,Student &s){
    //os<<s.name<<" "<<s.chinese<<" "<<s.math<<" "<<s.english<<" "<<s.total()<<endl;
    os<<s.name;
    return os;
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];    //因为重载了输入运算符,这里可以直接用cin>>a[i];
    }
    for(int i=1;i<n;i++){
        //从第i+1位开始的每位学生a[j],判断a[i]与a[j] 
        for(int j=i+1;j<=n;j++){
            if(a[i].check(a[j])){
                cout<<a[i]<<" "<<a[j]<<endl;    //因为重载了输出运算符,这里可以直接用cout<<a[i];
            }
        }
    }
    return 0;
}

指针

tangyj626阅读(323)

指针其实存储的是变量在内存中的地址,将指针指向变量后,就可以使用该指针来获取变量的值或者为变量赋新值。

一、指针的基本用法

指针是用来指向某个变量的,将指针指向变量后,指针存储的是该变量在内存中的地址。要使用指针,首先要申明指针,然后就可以将指针指向变量。将指针指向变量后,就可以使用该指针来获取变量的值或者为变量赋新值:

#include<iostream>
using namespace std;
int main()
{
	int n = 123;

    int *p;		//申明int*类型指针p,这样的指针可以指向int类型变量 
    p = &n;		//通过取地址运算将指针p指向变量n
 
    cout<<n<<endl;
	cout<<*p<<endl;	//通过指针获取其指向的变量的值

	*p = 0;			//通过指针修改其指向的变量的值 
	cout<<n<<endl;
	cout<<*p<<endl;

	(*p)++;			//指针指向变量的值自加,这里的括号不能省略 
	cout<<n<<endl;
	cout<<*p<<endl;

	int *q = p;		//指针q的值赋值为指针p的值,q也指向p指向的变量n 
	*q = *q + 1;
	cout<<n<<endl;
	cout<<*p<<" "<<*q<<endl;
    return 0;
}

指针实际上存储的是其指向的变量在内存中的地址,运行下面的程序:

#include<iostream>
using namespace std;
int main()
{
	int n = 123;
    int *p;		//申明int*指针p,可以指向int类型变量 
    p = &n;		//指针p指向变量n 
    cout<<n<<" "<<*p<<endl;		//变量的值 
	cout<<&n<<" "<<p<<endl;		//变量在内存的地址 
    return 0;
}

程序运行结果如下(自行测试时第2行可能和下面的结果不一致):

123 123
0x6dfe78 0x6dfe78

第2行输出的就是变量n在内存的地址(是一个十六进制数)。

对于scanf语句:scanf("%d",&n);,我们已经知道int类型变量n前面必须加上取地址符号&,现在我们清楚了,这里&n其实是指向变量n的指针,scanf函数要求从第2个参数开始后面的参数必须是指针,所以需要在普通变量前加上取地址符号&。

二、数组与指针

先来看下面的程序:

#include<iostream>
using namespace std;
int main()
{
	int a[5] = {1,2,3,4,5};
	cout<<a<<endl;
    return 0;
}

运行程序会发现,直接输出数组名a,输出的是一个十六进制的地址,原来数组名其实也是一个指针(其实是一个常量指针),指向的是数组的第0个元素。那么指针a+i指向的是数组第i个元素a[i]。来看下面的程序:

#include<iostream>
using namespace std;
int main()
{
	int a[5];
	for(int i=0;i<5;i++){
		scanf("%d",&a[i]);
	}
	for(int i=0;i<5;i++){
		printf("%d ",a[i]);
	}
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
	int a[5];
	for(int i=0;i<5;i++){
		scanf("%d",a+i);
	}
	for(int i=0;i<5;i++){
		printf("%d ",*(a+i));
	}
    return 0;
}

现在我们应该清楚scanf输入字符串时:char str[1010];scanf("%s",str);,这里用字符数组存储字符串,str已经是指针了,就不能在str前添加取地址符号&。

需要特别注意的是,数组名是一个常量指针,可以将数组名赋值给其他指针,但是不能将其他指针赋值给数组名:

#include<iostream>
using namespace std;
int main()
{
    int a[5];
    //指针pa赋值为数组名a,pa指向a[0]
    int *pa = a; 
    for(int i=0;i<5;i++){
        scanf("%d",pa+i);
    }
    for(int i=0;i<5;i++){
        printf("%d ",*(pa+i));
    }
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    int a[5],n = 100;
    int *pa = &n; 
    a = pa;  //编译错误 
    return 0;
}

三、结构体与指针

当然可以用一个指针指向结构体变量:

#include<iostream>
using namespace std;
struct Circle{
	double r;
	Circle(){}
	Circle(double r){	//构造函数 
		this->r = r;     //this是指向结构体自身的指针
	}
	double length(){	 //成员函数,计算返回圆周长 
		return 2*3.14*r;
	}
	double area(){	   //成员函数,计算返回圆面积 
		return 3.14*r*r;
	}
};
int main()
{
	Circle c(12.56);
	cout<<c.length()<<" "<<c.area()<<endl;
	Circle *p = &c;
    cout<<(*p).r<<" "<<(*p).length()<<" "<<(*p).area()<<endl;	//原始写法 
    cout<<p->r<<" "<<p->length()<<" "<<p->area()<<endl;		  //简便写法
    return 0;
}

甚至可以使用new关键字来直接生成指向结构体的指针:

#include<iostream>
using namespace std;
struct Circle{
	double r;
	Circle(){}
	Circle(double r){	//构造函数 
		this->r = r;
	}
	double length(){	 //成员函数,计算返回圆周长 
		return 2*3.14*r;
	}
	double area(){	   //成员函数,计算返回圆面积 
		return 3.14*r*r;
	}
};
int main()
{
	Circle *p = new Circle(12.56);
    cout<<(*p).r<<" "<<(*p).length()<<" "<<(*p).area()<<endl;	//原始写法 
    cout<<p->r<<" "<<p->length()<<" "<<p->area()<<endl;		  //简便写法
    return 0;
}

四、函数参数与指针

如果函数的参数是指针,那么在函数体内通过指针参数来修改指向变量的值,能够影响实际参数的值。将参数设置为指针类型也是函数参数采用地址传递的常用方法。

仍然以交换两个变量的值的函数为例,如果函数参数为指针,那么可以这样设计(注意和前面形式参数名前加&——引用传参——的区别):

#include<iostream>
using namespace std;
//参数是指针(地址传递)
void swap(int *x,int *y){
	int t = *x;
	*x = *y;
	*y = t;
}
int main()
{
    int a = 1,b = 2;
	cout<<a<<" "<<b<<endl;
	swap(&a,&b);
	cout<<a<<" "<<b<<endl;
    return 0;
}
#include<iostream>
using namespace std;
//引用传参
void swap(int &x,int &y){
	int t = x;
	x = y;
	y = t;
}
int main()
{
    int a = 1,b = 2;
	cout<<a<<" "<<b<<endl;
	swap(a,b);
	cout<<a<<" "<<b<<endl;
    return 0;
}

前面我们已经遇到过函数的参数是数组的情况,此时在函数体内修改形式参数数组元素的值,实际参数数组元素也会受影响。其实数组参数可以直接写成指针形式:

#include<iostream>
using namespace std;
int a[1001];
void read(int arr[],int n){
	for(int i=0;i<n;i++)
		cin>>arr[i];
}
void exec(int arr[],int n){
	for(int i=0;i<n;i++)
		arr[i] *= arr[i];
}
void out(int arr[],int n){
	for(int i=0;i<n;i++)
		cout<<arr[i]<<" ";
	cout<<endl;
}
int main()
{
    int n;
    cin>>n;
    read(a,n);
    out(a,n);
    exec(a,n);
    out(a,n);
    return 0;
}
#include<iostream>
using namespace std;
int a[1001];
void read(int *arr,int n){
	for(int i=0;i<n;i++)
		cin>>arr[i];
}
void exec(int *arr,int n){
	for(int i=0;i<n;i++)
		arr[i] *= arr[i];
}
void out(int *arr,int n){
	for(int i=0;i<n;i++)
		cout<<arr[i]<<" ";
	cout<<endl;
}
int main()
{
    int n;
    cin>>n;
    read(a,n);
    out(a,n);
    exec(a,n);
    out(a,n);
    return 0;
}

现在回顾一下使用scanf函数输入数据存储到变量中:

int n;
scanf("%d",&n);

调用scanf函数后,变量n的值会被修改成输入的整型数据(实际参数的值在函数内部被修改),scanf函数中实际参数n对应的形式参数就是指针类型。