C++ 函数

第五章(循环和关系表达式)和第六章(分支语句和逻辑运算符)直接跳过,所有语言都一样的,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,编译器会报错,如下

AF676F8B-B7F2-421C-8C1D-6CB5579F6063.png

将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;//获取指针
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351