before|正文之前:

c++实验代码及学习笔记(二)

你好! 这是一个高程实验课的代码记录及学习笔记。我将记录一些重要的知识点、易错点。但是作为大学生,水平很低,敬请指点教导、优化代码。
从本文起,我将尝试新的叙述风格,使得笔记更为易懂。O(∩_∩)O


1问题

首先我们看一下实验目标及要求:

FBIwarning:建议在阅读答案前,独立思考,先自行尝试,遇到问题再继续阅读。

2思路

这个实验涉及的知识非常基础/本质,没有考察算法等应用。如果只是单纯【应用】c++来解决问题的话,我们可能根本不会学习如何写头文件。但是,要求对这门语言更深的理解,建构完整的系统和思维方式,我们就需要掌握这类知识。

我对编程的一种理解是,它与机器交流的方式。如果用程序员和计算机对话的方式来看,是不是更可爱了呢?

趣解题目

老师的命令:二狗,你去写三个文件,第一个array.h头文件,第二个array.cpp源文件给我实现这四个功能,第三个main.cpp用来测试你写的函数。
二狗:这四个功能看起来不难!但是,头文件该怎么写呢?(老师:让你上课睡觉!)

1、头文件该怎么写

参考:C++中头文件(.h)和源文件(.cpp)都应该写些什么

  • 头文件是个什么东西
    最通俗的理解是,将别的文件中的代码插入到指定位置。就像复制粘贴一样,把别人家 的代码复制到咱这儿来。
    方便之处在于,将一些现成的,固有的定义、函数、代码、引用等等引入到你的编程中来,你就不需要再进行这样一些重复的工作了。
  • 头文件都有哪些东西
    写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现

第一步,我们建个工程,这是个好习惯,不要省略哦(这里埋下伏笔)
第二步,在头文件的文件夹里新建一个名为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

2、源文件该怎么写

源文件就是给出头文件具体实现的。

#include<iostream>
#include"array.h"
using namespace std;//具体实现头文件内设计的函数的代码
//代码略长,将在后文展出

需要注意的是:开头处包含了array.h,事实上,只要此cpp文件用到的文件,都要包含进来!这个文件的名字其实不一定要叫array.cpp,但非常建议cpp文件与头文件相对应。

二狗:老师我有一个问题,我们为什么要对头文件和源文件起一样的名字呢?
老师:这个呀是一种约定俗成的风俗,方便我们程序员阅读理解。其实什么名字对编译器是没有什么意义的,他很傻,只能识别#include等语句(二狗:那比我聪明不了多少耶)头文件和源文件的区别,.h文件和.cpp文件简单来说,在h文件中声明Declare,而在cpp文件中定义Define。
二狗:那么声明和定义又是什么呢(老师:你到底睡了多少节啊!!)
老师:咳咳,链接给你,自己去看 C语言中声明和定义详解,简单来讲呢,定义声明最重要的区别:定义创建了对象并为这个对象分配了内存,声明没有分配内存。函数的声明定义就更简单了,带有{ }的就是定义,否则就是声明。

头文件的作用:
二狗:计算机,我这里有四个函数,这个名字我先预定了,别的地方再也不能用它来作为变量名或对象名。
计算机:好。

源文件的作用:
二狗:计算机,你得给我这四个函数名分配内存,内存位置不能随便变。
计算机:好。

二狗:这样看来,计算机虽然傻乎乎的但是非常听话呀。

3、main.cpp测试

#include<iostream>
#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;
}

然后我们编译、运行就好了。

函数设计

回归到本题。学会写头文件、源文件后,我们需要关注这四个功能怎么解决。

  1. 将数组所有元素设置为一指定值
  2. 合并两个数组的内容,结果存入第三个数组
  3. 查找某个数在数组中的位置
  4. 从数组中删除某个数(有几个删几个)

首先我们从上下文推测,并从方便的角度来考虑,这个数组应该是int整型数组,储存的是正常的数值。参数都有数组指针。返回值为了主函数简洁我们可以在函数内打印,返回0。当然也可以尝试返回数组,函数更有实用价值(这里涉及到一个重要的知识点:思考数组能否被函数返回?)

其次,思考-突破,第一题非常简单,遍历数组,a[i] = x。第二题思路很简单,难度在于如何用简洁的代码写出,更为优雅。第三题我们用到break,需要一个index索引。结构上循环+判断即可。第四题是略有难度的一题,做法相当多(百度出至少三种),我们需要采取最容易实现的那种。

事实上算法的妙处在哪里呢?就在于方法很多,我们要想出效率最高、代码简洁的方法来解决实际问题,才是不断优化算法的价值所在。

关于设计接口,经验丰富的人在经过以上头脑风暴后能对数据类型、参数了然于心,而对于我们这种新手菜狗来说,容易陷入被水淹没不知所措的状态,不如先把结构写好后(如上文),先写源文件中的函数定义,把内容写好,再对照源文件中的函数接口写头文件。

2代码实现

  1. 第一个问题
    直接给出代码
#include<iostream>
#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;
}
  1. 第二个问题
    合并数组这一简单算法题,首先我们要知道,对于一般数组,一旦被创建,大小就固定了。数组的索引是从0开始的,也就是说,一个长度为n的数组,索引为0~(n-1)。
    这些题目也可以使用动态数组,c++可以使用向量数组vector,将会更为方便。
    但是新手二狗并未接触vector,我们可以在后文讨论

数组实例是从System.Array继承的对象,数组是引用类型,有数据的引用及数据对象本身,引用在栈或堆上,且数组本身总是在堆上。
合并数组的算法一般分为两种,一种是两个有序数组的合并,合并完后保证数组依然有序,还有一种是两个数组合并并对合并的数组进行排序。2

这里,我们讨论第二种无序的数组合并,题目中没有涉及排序,若涉及排序,请参考这篇文章:简单的算法题之合并数组
我参考的代码是c#的,c#与我学过的java风格非常像,一开始我竟然没有分辨出来。

在c#或者java中,都可以用arr.length求得数组长度。
所以习惯java的我也这么写了,但是……c语言和c++没有。非常悲剧,导致全篇代码都要改。这里插播一下,c/c++该如何求数组长度呢?
详细原理阅读C++中获取静态数组和动态数组的长度

  • 字符串数组
    可以用strlen()函数获取
  • 一般数组
    可以用sizeof(a)/sizeof(a[0])来获取数组的长度
    但是!在自定义函数中不可用!因为函数中传来的参数是数组的指针,用sizeof求出来的是地址的长度。
  • 函数中数组
    1动态数组,c++ vector
    C++中vector使用详细说明 (转)
    2在主函数中sizeof求后,将长度参数传到自定义函数中

参考代码

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;
  1. 第三个问题
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;}elseindex = -1;}if(index == -1)cout << "该数不在数组中" <<endl;return index;
}
  1. 第四个问题

删除数组元素这个算法题有不少解决方案,目前有普遍三种。
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;
}

3测试代码

下面我们写main.cpp的内容
因为发现c++输入很方便所以增加了输入流的内容??

#include<iostream>
#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……嗯。

4最终效果

那么,我们来看一下最终成果吧!
记得所有文件在一个工程里哟~

5额外补充

数组能否被返回

大家会注意到我代码中会出现int*指针函数,返回值是arr。这是否说明c++自定义函数能被返回呢?
其实不是的。第一,本题其实没有必要返回数组,前期我的思路是在main函数中打印等等,所以写成了把数组返回的形式,报错之后将错就错,查找了改正方式,把正确的形式留了下来。
第二,我们需要知道:

C 语言不允许返回一个完整的数组作为函数的参数。

但是,我们可以通过指定不带索引的数组名来返回一个指向数组的指针
也可以以指针变量作为函数参数,来实现数组的返回(并不是直接作为返回值)。
参考文章:C/C++中如何接收return返回来的数组元素

一、返回传入数组指针的方式

首先我们来看看这种方法所涉及的知识:(1)指针函数。C语言中允许一个函数返回值是一个指针(地址)基本格式是: 基类型 * 函数名(参数列表)(2)静态变量与局部变量。我们知道C语言程序在运行时,操作系统会给其分配内存空间。这段空间主要分为四个区域,分别是栈取,堆区,数据区,代码区。那么静态变量是存放在数据区,作用范围是全局的,在内存中只存储一份。局部变量通常放在栈中,随着被调用的函数的退出内存空间自动释放。 要接收被调函数返回的指针,那么可以使用一个指针变量。关键是被调函数用什么去返回数组的首地址,正如前面所说,被调函数在执行完之后内存空间就被释放。3

这里提供两种方法解决这一问题:1)通过传入一个空的数组头地址,返回这个变量

//通过返回传入数组的指针的方式
#include"stdio.h"
#include<iostream>
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<stdio.h>
//定义产生数组的函数
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<iostream>
//定义一个以指针变量作为形参的函数,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);   //传入数组awhile(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掉,防止内存泄露!


感谢大家阅读~
不知道大家喜不喜欢二狗的语言风格呢⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
喜欢的话就双击关注 点个赞叭

6参考文章


  1. https://www.cnblogs.com/fenghuan/p/4794514.html ↩︎

  2. https://www.cnblogs.com/Ribbon/p/5916855.html ↩︎

  3. https://www.cnblogs.com/Wade-James/p/7965775.html ↩︎

  4. https://www.cnblogs.com/Wade-James/p/7965775.html ↩︎

  5. http://www.dotcpp.com/wp/755.html ↩︎

c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例相关推荐

  1. 第五十七篇:VS2015建立一个完整的c++工程:头文件.h 源文件.cpp,自动生成类

    之前没有用VS或者在vs中一个源程序写到底,没有使用C++编一个工程 打开VS2015 ,新建VS win32工程,前面步骤很简单,不再阐述 下面直接开始: 新建一个VC++ win32的程序, 在源 ...

  2. 个人学习之C++ 头文件.h与.cpp

    最近在学习的过程中发现这个问题,因为是学习所以内容对网上的内容有很多参考 在一个C++程序中,只包含两类文件--.cpp文件和.h文件. 一.初步了解 1.头文件的作用:  方便函数的统一的声明 2. ...

  3. extern与头文件(*.h)的区别和联系

    原文网址为:http://lpy999.blog.163.com/blog/static/117372061201182051413310/ 个人认为有一些道理:所以转过来学习了. 用#include ...

  4. C语言头文件.h互相包含所引发的一系列错误C2143之类的解决方法

    本文可解决的问题: 在一个头文件.h中定义一个结构体,在另一个.h文件中使用这个结构体引发错误 C2143    语法错误: 缺少")"(在"*"的前面) (编 ...

  5. c++ 头文件的创建和使用,头文件与源文件的命名关系,#include <头文件> 和 #include “头文件“的关系

    创建头文件的三个步骤 以实现一个swap函数为例: 1.创建一个.h的头文件:里面允许存放类声明和函数声明 // swap.h #pragma oncevoid swaps(int a, int b) ...

  6. static和头文件,源文件放什么

    目录 头文件or源文件 1.局部静态变量 2.全局静态变量/普通的全局静态函数 3.静态数据成员.成员函数(面向对象) 头文件or源文件 a)头文件应该放: 1)普通的全局函数声明 2)普通的全局变量 ...

  7. c语言的函数头书写标准,C语言的头文件的函数和书写方法.doc

    C语言的头文件的函数和书写方法 C语言头文件作用及写法 头文件几个好处: 1,头文件可以定义所用的函数列表,方便查阅你可以调用的函数:2,头文件可以定义很多宏定义,就是一些全局静态变量的定义,在这样的 ...

  8. vim中,c/cpp文件如何在头文件和.c/.cpp文件中快速的进行跳转

    vim中,c/cpp文件如何在头文件和.c/.cpp文件中快速的进行跳转   大家好,我叫亓官劼(qí guān jié ),在CSDN中记录学习的点滴历程,时光荏苒,未来可期,加油~博客地址为:亓官 ...

  9. 单片机零基础入门(9-1)实战:模块化编程(模块化两个案例含源码--以及无法显示头文件(.h)的解决方案)

    单片机零基础入门(9-1)实战:模块化编程-(以及无法显示头文件(.h)的解决方案) 本文作为单片机零基础入门(8-5)模块化编程的拓展和补充,比前面的单片机零基础入门(8-5)模块化编程更为详细. ...

  10. C++之头文件与源文件

    (转) 一.C++编译模式     通常,在一个C++程序中,只包含两类文件--.cpp文件和.h文件.其中,.cpp文件被称作C++源文件,里面放的都是C++的源代码:而.h文件则被称作C++头文件 ...

最新文章

  1. build with runtime package
  2. 【转】matlab函数_连通区域
  3. Flask + Nginx + React + Webpack 配置解决跨域问题
  4. Vue + webpack 项目实践
  5. 谈慎独2017-12-19
  6. Springboot整合RabbitMq-用心看完这一篇就够了(最新)
  7. 牛客网Java刷题知识点之数组、链表、哈希表、 红黑二叉树
  8. python加载html表格数据,使用python 3.6获取html表格行数据美丽的汤
  9. 硬件工程师在笔试中容易遇到的题目
  10. windows快速关闭有效方法2则
  11. oracle trace跟踪,浅析Oracle追踪工具SQL Trace的启用方式
  12. mysql 主从 不一致_揭秘MySQL主从数据不一致
  13. C++指针、空指针、野指针使用的一些总结
  14. C语言的32个关键字和9种控制语句
  15. 基于stm32单片机智能温控风扇控制系统Proteus仿真(源码+仿真+全套资料)
  16. 别做老好人,你的善良应该带点锋芒
  17. 测试软件的稳定性,客户端稳定性测试
  18. 量子计算机的成熟度模型,全球首家:紫光展锐通过 TMMi 软件测试成熟度模型集成 5 级认证...
  19. 保存二维码图片到手机相册
  20. 神级程序员都在用什么工具?【建议收藏】

热门文章

  1. 思科交换机工作模式及基本命令
  2. dialog沉浸式状态栏android,Dialog全屏,去掉状态栏的方式
  3. 数据分析项目-大选献金数据分析
  4. 智牛股_第6章_Mybatis Generator代码生成器
  5. 基层教学组织评估系统6_项目完结心得收获、思考人生篇
  6. python抓取豆瓣电影
  7. linux添加fcitx输入法,linux安装输入法_怎么在Linux下安装fcitx输入法
  8. 嵌入式linux dlna,DLNA 编译
  9. 服务器主板能配固态硬盘吗,旧主板洗洗还能用、Z490主板搭配PCIe4.0固态硬盘 测试...
  10. 计算机行业的薪资真的有那么高吗?讲真,有的一毕业就失业,有的一毕业就拿 20k+