c++实验代码及学习笔记(二)
你好! 这是一个高程实验课的代码记录及学习笔记。我将记录一些重要的知识点、易错点。但是作为大学生,水平很低,敬请指点教导、优化代码。
从本文起,我将尝试新的叙述风格,使得笔记更为易懂。O(∩_∩)O
首先我们看一下实验目标及要求:
FBIwarning:建议在阅读答案前,独立思考,先自行尝试,遇到问题再继续阅读。
这个实验涉及的知识非常基础/本质,没有考察算法等应用。如果只是单纯【应用】c++来解决问题的话,我们可能根本不会学习如何写头文件。但是,要求对这门语言更深的理解,建构完整的系统和思维方式,我们就需要掌握这类知识。
我对编程的一种理解是,它与机器交流的方式。如果用程序员和计算机对话的方式来看,是不是更可爱了呢?
老师的命令:二狗,你去写三个文件,第一个array.h头文件,第二个array.cpp源文件给我实现这四个功能,第三个main.cpp用来测试你写的函数。
二狗:这四个功能看起来不难!但是,头文件该怎么写呢?(老师:让你上课睡觉!)
参考:C++中头文件(.h)和源文件(.cpp)都应该写些什么
第一步,我们建个工程,这是个好习惯,不要省略哦(这里埋下伏笔)
第二步,在头文件的文件夹里新建一个名为array.h的头文件,写头文件要注意嗷,在开头和结尾处必须按照如下样式加上预编译语句:
第一种
#ifndef ARRAY_H
#define ARRAY_H
//你的代码写在这里
#endif
第二种(可能老旧编译器不支持)
#pragma once
//你的代码写在这里
目的是啥呢,就是告诉计算机只能编译一次,防止重复编译出bug嗷。
至于ARRAY_H那个名字叫什么其实随意,属于编译器并不能识别的人类语。但是呢最好要跟头文件名字一样,对应起来非常方便。
第三步,写头文件,可以在头文件中写类,class、public之类的,但是我们这里不用,简单的设计函数接口就行。
#ifndef __ARRAY_H__
#define __ARRAY_H__
int *setArr(int*,int,int); //全部元素设为同一值
int mergeArr(int*,int*,int,int); //合并两个数组,结果存入第三个数组
int searchArr(int*,int,int); //查找某个数在数组中的位置
int *deleteArr(int*,int,int); //从数组中删除某个数,有几个删几个
#endif
二狗小贴士
.h叫做头文件,它是不能被编译的。“#include”叫做编译预处理指令,可以简单理解成,在1.cpp中的#include"1.h"指令把1.h中的代码在编译前添加到了1.cpp的头部。每个.cpp文件会被编译,生成一个.obj文件,然后所有的.obj文件链接起来你的可执行程序就算生成了。发现了没有,你要在.h文件中严格区分声明语句和定义语句。好的习惯是,头文件中应只处理常量、变量、函数以及类等等等等的声明,变量的定义和函数的实现等等等等都应该在源文件.cpp中进行。1
源文件就是给出头文件具体实现的。
#include
#include"array.h"
using namespace std;
//具体实现头文件内设计的函数的代码
//代码略长,将在后文展出
需要注意的是:开头处包含了array.h,事实上,只要此cpp文件用到的文件,都要包含进来!这个文件的名字其实不一定要叫array.cpp,但非常建议cpp文件与头文件相对应。
二狗:老师我有一个问题,我们为什么要对头文件和源文件起一样的名字呢?
老师:这个呀是一种约定俗成的风俗,方便我们程序员阅读理解。其实什么名字对编译器是没有什么意义的,他很傻,只能识别#include等语句(二狗:那比我聪明不了多少耶)头文件和源文件的区别,.h文件和.cpp文件简单来说,在h文件中声明Declare,而在cpp文件中定义Define。
二狗:那么声明和定义又是什么呢(老师:你到底睡了多少节啊!!)
老师:咳咳,链接给你,自己去看 C语言中声明和定义详解,简单来讲呢,定义声明最重要的区别:定义创建了对象并为这个对象分配了内存,声明没有分配内存。函数的声明定义就更简单了,带有{ }的就是定义,否则就是声明。
头文件的作用:
二狗:计算机,我这里有四个函数,这个名字我先预定了,别的地方再也不能用它来作为变量名或对象名。
计算机:好。
源文件的作用:
二狗:计算机,你得给我这四个函数名分配内存,内存位置不能随便变。
计算机:好。
二狗:这样看来,计算机虽然傻乎乎的但是非常听话呀。
#include
#include"array.h" //要把array.h #include进去
using namespace std;
int main()
{
int ...//此处省略
mergeArr(arr1,arr2,len1,len2);
searchArr(arr,value,len);
deleteArr(arr,y,len);
setArr(arr,x,len);
return 0;
}
然后我们编译、运行就好了。
回归到本题。学会写头文件、源文件后,我们需要关注这四个功能怎么解决。
首先我们从上下文推测,并从方便的角度来考虑,这个数组应该是int整型数组,储存的是正常的数值。参数都有数组指针。返回值为了主函数简洁我们可以在函数内打印,返回0。当然也可以尝试返回数组,函数更有实用价值(这里涉及到一个重要的知识点:思考数组能否被函数返回?)
其次,思考-突破,第一题非常简单,遍历数组,a[i] = x。第二题思路很简单,难度在于如何用简洁的代码写出,更为优雅。第三题我们用到break,需要一个index索引。结构上循环+判断即可。第四题是略有难度的一题,做法相当多(百度出至少三种),我们需要采取最容易实现的那种。
事实上算法的妙处在哪里呢?就在于方法很多,我们要想出效率最高、代码简洁的方法来解决实际问题,才是不断优化算法的价值所在。
关于设计接口,经验丰富的人在经过以上头脑风暴后能对数据类型、参数了然于心,而对于我们这种新手菜狗来说,容易陷入被水淹没不知所措的状态,不如先把结构写好后(如上文),先写源文件中的函数定义,把内容写好,再对照源文件中的函数接口写头文件。
#include
#include"array.h"
using namespace std;
int *setArr(int arr[],int x,int len)//全部元素设为同一值
{
cout << "重新设置元素全部为" << x << "后的数组" <<endl;
for(int i=0;i<len;i++){
arr[i] = x;
cout << arr[i] << " ";
}
return arr;
}
数组实例是从System.Array继承的对象,数组是引用类型,有数据的引用及数据对象本身,引用在栈或堆上,且数组本身总是在堆上。
合并数组的算法一般分为两种,一种是两个有序数组的合并,合并完后保证数组依然有序,还有一种是两个数组合并并对合并的数组进行排序。2
这里,我们讨论第二种无序的数组合并,题目中没有涉及排序,若涉及排序,请参考这篇文章:简单的算法题之合并数组
我参考的代码是c#的,c#与我学过的java风格非常像,一开始我竟然没有分辨出来。
在c#或者java中,都可以用arr.length求得数组长度。
所以习惯java的我也这么写了,但是……c语言和c++没有。非常悲剧,导致全篇代码都要改。这里插播一下,c/c++该如何求数组长度呢?
详细原理阅读C++中获取静态数组和动态数组的长度
参考代码
int mergeArr(int arr1[],int arr2[],int len1,int len2)//合并两个数组,结果存入第三个数组
{
cout << "合并数组,结果存入arr3:" <<endl;
int *newArr = new int[len1 + len2];
int k = 0;
for(int i=0;i<len1;i++)
{
newArr[k++] = arr1[i];
}
for(int i=0;i<len2;i++)
{
newArr[k++] = arr2[i];
}
for(int i=0;i<(len1+len2);i++)
{
cout << "arr3[" << i << "] = " <<newArr[i] <<endl;
}
return 0;
int searchArr(int arr[],int value,int len) //查找某个数在数组中的位置
{
int index;
for(int i=0;i<len;i++)
{
if(arr[i] == value){
index = i;
cout << value << "在数组中的位置是第" << i <<"个"<<endl;
break;
}
else
index = -1;
}
if(index == -1)
cout << "该数不在数组中" <<endl;
return index;
}
删除数组元素这个算法题有不少解决方案,目前有普遍三种。
1.可以将其要删除的元素置为null,然后遍历这个数组的时候,需要将数组中出现null的过滤掉
2.可以让被删除的元素的后面的元素,集体的想左移动,然后减小数组的长度。
3.可以最后一个元素代替你要替换的元素,然后减少数组的长度。
我们这里由于可能不止删除一个元素,且第三个方法会破坏数组顺序,所以采用“逆向思维法”,算是第一种思路的变体,既然删除麻烦,那么我就保留。只要数组中没有该元素(a[i] != value),就保留下去,成为新的数组。这样的操作我认为是最简单的。
这个巧妙的方法当然不是我想出来的。感谢博主AllSight
参考文章C语言 · 删除数组中的0元素
int *deleteArr(int arr[],int y,int len) //从数组中删除某个数,有几个删几个
{
int j = 0;
cout << "删除" << y <<"后的arr:{";
for(int i=0;i<len;i++)
{
if(arr[i] != y)
{
arr[j] = arr[i];
j++;
cout <<arr[i]<<" ";
}
}
cout << "}" <<endl;
return arr;
}
下面我们写main.cpp的内容
因为发现c++输入很方便所以增加了输入流的内容??
#include
#include"array.h" //在这里包含
using namespace std;
int main()
{
int arr[] = {1,2,3,4,5,5};
int arr1[] = {5,4,3,2,1};
int arr2[] = {1,2,3};
int x,y,value;
int len = sizeof(arr)/sizeof(arr[0]);
int len1 = sizeof(arr1)/sizeof(arr1[0]);
int len2 = sizeof(arr2)/sizeof(arr2[0]);
cout << "1、合并两个数组" << endl;
mergeArr(arr1,arr2,len1,len2);
cout << "2、请输入一个数字,在数组中查找该元素" << endl;
cin >> value;
searchArr(arr,value,len);
cout << "3、请输入一个数字,在数组中删除该元素" << endl;
cin >> y;
deleteArr(arr,y,len);
cout << "4、请输入一个数字,将数组所有元素设为该数" << endl;
cin >> x;
setArr(arr,x,len);
return 0;
}
一开始我用dev c++这个IDE,没有建立工程习惯的我直接扑街,一编译就报错,说没有声明这些函数。我??? 于是浪费了一个小时时间总算查清楚需要建立工程,把三个文件放入一个工程下。
换了VS2017之后,一进去就是新建工程,再从工程中新建CPP……嗯。
那么,我们来看一下最终成果吧!
记得所有文件在一个工程里哟~
大家会注意到我代码中会出现int*指针函数,返回值是arr。这是否说明c++自定义函数能被返回呢?
其实不是的。第一,本题其实没有必要返回数组,前期我的思路是在main函数中打印等等,所以写成了把数组返回的形式,报错之后将错就错,查找了改正方式,把正确的形式留了下来。
第二,我们需要知道:
C 语言不允许返回一个完整的数组作为函数的参数。
但是,我们可以通过指定不带索引的数组名来返回一个指向数组的指针。
也可以以指针变量作为函数参数,来实现数组的返回(并不是直接作为返回值)。
参考文章:C/C++中如何接收return返回来的数组元素
首先我们来看看这种方法所涉及的知识:(1)指针函数。C语言中允许一个函数返回值是一个指针(地址)基本格式是: 基类型 * 函数名(参数列表)(2)静态变量与局部变量。我们知道C语言程序在运行时,操作系统会给其分配内存空间。这段空间主要分为四个区域,分别是栈取,堆区,数据区,代码区。那么静态变量是存放在数据区,作用范围是全局的,在内存中只存储一份。局部变量通常放在栈中,随着被调用的函数的退出内存空间自动释放。 要接收被调函数返回的指针,那么可以使用一个指针变量。关键是被调函数用什么去返回数组的首地址,正如前面所说,被调函数在执行完之后内存空间就被释放。3
这里提供两种方法解决这一问题:1)通过传入一个空的数组头地址,返回这个变量
//通过返回传入数组的指针的方式
#include"stdio.h"
#include
using namespace std;
//定义指针函数
int *copy(int array[], int a[], int n);
int main(){
int size = 4;
int a2[4];
int a1[4] = {3, 5, 7 ,8};
int *p; //定义指针变量!
p = copy(a1, a2, size); //通过返回main函数中的a数组的首地址,将其付给指针变量p,
//从而达到数组传递的作用。
cout << p[0] << " " << p[1] << " "<<p[2] << " " << p[3] << endl;
return 0;
}
int *copy(int array[], int a[], int n)
{
for(int i = 0; i < n; i++)
a[i] = array[i];
return a;
}
2)C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。
//使用静态变量进行返回
#include
//定义产生数组的函数
int *TestFuction();
int main(){
int *p;
p = TestFuction();
while(*p != 0){
printf("%d", &p);
p++;
}
return 0;
}
int *TestFuction(){ //可以不传入参数
static int test[5] = {8, 4, 5, 2, 7};
return test;
}
test数组是一个静态变量,在被调函数执行完成之后不会被释放
指针变量变量需要动态分配内存,通常放在堆区中,该区域内通常由程序员分配或释放。将要处理的数组的首地址以实参的形式传递给函数处理,处理完后的指针是和实参的数组同一块地址,达到返回数组的效果。4
人类语
实参:函数君,给你5毛钱,帮我整一个!
函数君:没问题!
(@#¥……)
函数君:糟了,我,我的内存要释放了……地址,你的地址……
实参:还好我给你的参数地址一直在常量区,溜了溜了
函数君:你,你……好你个实参……啊!
调用实参,委托被调用方(函数君)进行操作,由于此局部变量属于调用方本身,故即便函数君结束内存释放,也不会被影响到该数组,达到曲线救国的目的。
示例代码如下:
//使用指针变量作为函数参数,来实现数组的返回
#include
//定义一个以指针变量作为形参的函数,n作为循环次数
void SumTest(int *p, int n);
using namespace std;
int main2(){
int i = 0;
int a[5] = {8, 5, 3, 2, 6};
SumTest(a, 5); //传入数组a
while(i < 5){
cout << a[i] << " ";
i++;
}
cout << endl;
return 0;
}
void SumTest(int *p, int n){
int i = 0;
while(i < n){
*p = *p + 1;
p++;
i++;
}
}
参考文章:C语言自定义函数如何返回数组(上)?
C语言自定义函数如何返回数组(下)?
C语言中,我们通常用malloc来在堆区动态开辟内存,利用堆区“现用现开辟,用完手动收回”特点,实现灵活管理。是实际开发中的常用办法,也是我们今天的主要内容。
由于动态开辟内存在堆区,堆区不想上一讲中局部变量在栈区存储,系统根据它的生命周期自动收回,而是手动开辟,手动释放,这样就可以完全规避问题,例子与效果见下图:5
需要注意的是:记得用完free掉,防止内存泄露!
感谢大家阅读~
不知道大家喜不喜欢二狗的语言风格呢⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
喜欢的话就双击关注 点个赞叭
https://www.cnblogs.com/fenghuan/p/4794514.html ↩︎
https://www.cnblogs.com/Ribbon/p/5916855.html ↩︎
https://www.cnblogs.com/Wade-James/p/7965775.html ↩︎
https://www.cnblogs.com/Wade-James/p/7965775.html ↩︎
http://www.dotcpp.com/wp/755.html ↩︎