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

before|正文之前:

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

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


1问题

首先我们看一下实验目标及要求:
c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例_第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
#include"array.h"
using namespace std;

//具体实现头文件内设计的函数的代码
//代码略长,将在后文展出

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

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

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

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

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

3、main.cpp测试

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

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

函数设计

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

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

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

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

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

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

2代码实现

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

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

这里,我们讨论第二种无序的数组合并,题目中没有涉及排序,若涉及排序,请参考这篇文章:简单的算法题之合并数组
我参考的代码是c#的,c#与我学过的java风格非常像,一开始我竟然没有分辨出来。
c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例_第2张图片
在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;
		}
		else
			index = -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
#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最终效果

那么,我们来看一下最终成果吧!
记得所有文件在一个工程里哟~
c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例_第3张图片
c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例_第4张图片

5额外补充

数组能否被返回

大家会注意到我代码中会出现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掉,防止内存泄露!

c/c++ 头文件(.h)、源文件(.cpp)书写及接口与实现分离实例_第5张图片
感谢大家阅读~
不知道大家喜不喜欢二狗的语言风格呢⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
喜欢的话就双击关注 点个赞叭

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++,学习笔记)