第五章(循环和关系表达式)和第六章(分支语句和逻辑运算符)直接跳过,所有语言都一样的,if/else/switch/while/for这种。直接进入第七章(函数)。
1.函数原型
以下是函数原型的例子
void cheers(int);//cheers方法的函数原型
int main()
{
using namespace std;
cheers(5);
return 0;
}
//函数的实际实现
void cheers(int n)
{
using namespace std;
for(int i=0;i<n;i++){
cout << "Cheers! ";
}
cout << endl;
}
避免使用函数原型的唯一方法是,在首次使用函数之前定义它。函数原型不要求提供变量名,有类型列表就足够了。
函数原型的作用:
- 确保编译器正确处理函数返回值;
- 编译器检查使用的参数数目是否正确;
- 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。
2.函数参数和按值传递
举个栗子
double volume = cube(side);
cube的原型如下:
double cube(double x);
被调用时,该函数将创建一个新的名为x的double变量,并将其初始化。这样cube执行的操作将不会影响原数据,因为cube()使用的是side的副本,而不是原来的数据(C/C+新手的话,一定要注意这块)。
3.函数和数组
还是先举个栗子
int sum_arr(int arr[],int n);// n是arr的size
表面上看arr是个数组,但是实际上arr是个指针。在C++中,当且仅当用于函数头或函数原型中,int *arr 和 int arr[]的含义是相同的。它们都意味着arr是一个int指针。这块是个知识点,面试题经常会问,数组在函数参数时是退化为指针的。不明白的同学可以尝试理解下下面的代码
#include <iostream>
void sizeOfArray(int arr[])
{
//函数中,arr从数组退化成指针
using namespace std;
cout << "in func arr size:" << sizeof(arr) << endl;
}
int main() {
using namespace std;
int arr[10];
cout << "arr size:" << sizeof(arr) << endl;//输出的值为sizeof(int)*10
sizeOfArray(arr);//输出的值为指针所占的字节数,64位mac为8
return 0;
}
将数组地址作为参数的好处是可以节省复制整个数组所需的时间和内存。如果数组很大,使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花时间复制大块的数据。坏处是使用原数据增加了破坏数据的风险,可以使用const保护数组,如下:
void show_array(const int arr[],int size);//函数原型
如果尝试在show_array的实现中尝试修改arr,编译器会报错,如下
将const用于指针有一些很微妙的地方。可以用两种不同的方式将const用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。举个栗子:
int age = 39;
const int *pt = &age;//一个指针,指向的是const int
*pt = 1;//不可以,因为指针指向的是const int
age = 20;//可以,因为age本身只是int,是可变的
const float g_earth = 9.80;
const float *pe = &g_earth;//可以,常量
const float g_moon = 9.99;
float * pm = &g_moon;//不可以,C++禁止这种情况
如果将指针指向指针,也是类似的规则,C++不允许如下的情况
const int **pp;
int *p;
pp = &p;//不可以,编译器报错
结论:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给const的指针,但只能将非const数据的地址赋给const指针。
并且建议尽可能将指针参数声明为指向常量数据的指针,理由如下:
- 可以避免由于无意间修改数据而导致的编程错误;
- 使用const使得函数能够处理const和非const实参,否则将只能接受非const数据。
再介绍下常量指针,如下
int a = 3;
int b = 4;
int * const pt = &a;//可以
pt = &b;//不可以
pt是一个常量指针,初始化后将不能再修改指向位置。
4.函数和二维数组
考虑如下情况:
int data[3][4];
int total = sum(data,3);
sum的函数原型应该是啥样?答案是
int sum(int (*a)[4],int size);
这里要注意的是,多维数组的话,是有第一维会退化成指针,后面的维度都是还是数组。并且第一个参数应该是int (*a)[4],而不是int *a[4],因为int *a[4]表示一个由4个指向int的指针组成的数组。sum的另一种可读性更强的原型是
int sum(int a[][4],int size);
如果对于上面的描述理解不上去的,可以结合下面的代码感受下
int val = 20;
int valArray[3][4];
int *ptArray[4];//指针数组,也可以这么理解 (int *)ptArray[4],ptArray是一个数组,每个元素 int*
int *pt = &val;
ptArray[0] = pt;
int (*arrArray)[4];//arrArray是个指针,指针指向的每个元素是个int [4]类型
arrArray = valArray;//可以
ptArray = valArray;//不可以, 编译器报错,类型不对
5.函数和C-风格字符串
先温习下C-风格字符串,表示的方式有三种:
- char数组;
- 用引号括起的字符串常量(也称字符串字面值);
- 被设置为字符串的地址的char指针。
上述其实说的都是char指针(char*),因此函数原型参数都为如下
void processCStr(char *);
C风格字符串与常规char数组的一种重要区别是字符串有内置的结束字符。这意味着不必将字符串长度作为参数传递给函数,函数可以使用循环检查字符串中的每个字符直到遇到结尾的空字符。
返回的字符串的方式如下:
char* returnStr(){
char *s = "string";
return s;
}
int main() {
using namespace std;
cout << "result:" << returnStr() << endl;
return 0;
}
6.函数和结构体
结构体和普通变量类似,函数都将创建参数对应的副本,函数内操作的其实是结构体变量的副本,所以在结构体变量包含的数据较多时,会有性能问题。有两种方式可以提高效率,第一种是传递结构体的指针,第二种是传递结构体的引用(关于引用会在下一章讲解),简单举个栗子:
#include <iostream>
struct Person {
int age;
char *name;
};
//在函数内的操作将不影响原值
void processStruct1(Person p) {
p.name = "mrlee1";
p.age = 20;
}
void processStruct2(Person *p) {
p->name = "mrlee2";
p->age = 21;
}
void processStruct3(Person &p) {
p.name = "mrlee3";
p.age = 22;
}
void printPerson(Person p) {
using namespace std;
cout << "age:" << p.age << ",name:" << p.name << endl;
}
int main() {
using namespace std;
Person originPerson = {18, "mrlee"};
//按值传递
processStruct1(originPerson);
printPerson(originPerson);//实际打印age:18,name:mrlee,没有变化
//指针传递
processStruct2(&originPerson);
printPerson(originPerson);//实际打印age:21,name:mrlee2
//引用传递
processStruct3(originPerson);
printPerson(originPerson);//实际打印age:22,name:mrlee3
return 0;
}
7.函数和string对象
虽然C风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构体更相似。例如,可以将一个结构体赋给另一个结构体,也可以将一个对象赋给结构体。如果需要多个字符串,可以声明一个string对象数组,而且不是二维char数组。举个栗子:
#include <iostream>
#include <sstream>
using namespace std;
void processSting(const std::string strings[], int size) {
for (int i = 0; i < size; i++) {
cout << "string[" << i << "]:" << strings[i] << endl;
}
}
/**
* 这个比较尴尬,因为C++里int转string还是比较麻烦,目前先这么写了
* @param n
* @return
*/
string intToString(int n) {
stringstream stream;
stream << n;
return stream.str();
}
int main() {
const int size = 5;
string strings[size];
for (int i = 0; i < size; i++) {
strings[i] = "mrlee" + intToString(i + 1);
}
processSting(strings, size);
return 0;
}
8.函数与array对象
没啥好说的,看栗子吧:
#include <iostream>
#include <sstream>
#include <array>
#include <string>
using namespace std;
const int size = 4;
/**
* 这个比较尴尬,因为C++里int转string还是比较麻烦,目前先这么写了
* @param n
* @return
*/
string intToString(int n) {
stringstream stream;
stream << n;
return stream.str();
}
//按值传递,函数处理的是原始对象的副本
void wrongModifyArray(array<string, size> stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
stringArray[i] = "modified1";
}
}
//按指针传递
void rightModifyArray(array<string, size> *stringArray) {
for (int i = 0; i < (*stringArray).size(); i++) {
(*stringArray)[i] = "modified" + intToString(i);
}
}
//按引用传递
void rightModifyArray2(array<string, size> &stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
stringArray[i] = "modified2" + intToString(i);
}
}
void printArray(array<string, size> stringArray) {
for (int i = 0; i < stringArray.size(); i++) {
cout << "string" << i << ":" << stringArray[i] << endl;
}
cout << endl;
}
int main() {
array<string, size> originStringArray = {
"string1", "string2", "string3", "string4"
};
printArray(originStringArray);
wrongModifyArray(originStringArray);
printArray(originStringArray);
rightModifyArray(&originStringArray);
printArray(originStringArray);
rightModifyArray2(originStringArray);
printArray(originStringArray);
return 0;
}
打印结果如下
string0:string1
string1:string2
string2:string3
string3:string4
string0:string1
string1:string2
string2:string3
string3:string4
string0:modified0
string1:modified1
string2:modified2
string3:modified3
string0:modified20
string1:modified21
string2:modified22
string3:modified23
9.函数指针
函数这个话题比较大,这里只是简单点一下。
与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。还是看个栗子吧:
void func(string name) {
cout << "hello " << name << endl;
}
int main() {
//声明函数指针
void (*funcPt)(string);
//获取函数的地址,就是函数名其实
funcPt = func;
//使用指针调用函数
funcPt("mr.lee");
//下面方式也可以,个人倾向于下面这种,虽然写的对了,但是表示的比较明确
(*funcPt)("mr.lau");
return 0;
}
通常,要声明指向特定类型的函数的指针,可以首先编写这个函数的原型,然后用形如(*pt)替换函数名即可。
下面解释一个稍微复杂的栗子:
const double *(*pa[3])(const double *,int) = {f1,f2,f3};
在解释这个复杂的栗子之前首先回顾一点东西
int *a[3];//a是一个数组,每个元素是int *;
int (*b)[3];//b是一个指针,指针指向的每个元素都是int[3]
f1是什么类型的?
来逐步解释,首先运算符[]优先级高于,因此pa[3]表示p3是一个数组,这个数组包含三个元素,每个元素是指针类型。那是什么指针类型呢?是一个返回const double *,参数是const double *和int的函数,所以f1的声明如下
const double * f1(const double *,int)
上面的代码比较冗长,可以考虑使用typedef简化,先看下简单的typedef如何使用
typedef double real;
int main() {
real a = 5.4;
return 0;
}
再看如何简化上面的函数指针
typedef double * (*p_fun)(const double *,int);//声明别名
const double * f1(const double *,int);//声明函数原型
p_fun func_pt = f1;//获取指针
网友评论