[Relearning C/Cpp] Initialization and Functions
1 初始化
对象的声明可以通过初始化的过程获得初始值。主要就是见识一下C/Cpp里面几种初始化的形式,免得大惊小怪
1 | // C语言的初始化还好,怎么着都得有个 = |
对于C来说,有显示初始化(Explicit initialization),隐式初始化(Implicit initialization),零初始化(Zero initialization)
显示的比较好理解,scalar initialization,array initialization,struct initialization,基本都没啥好说的,经常用
隐式初始化就是你没给它显示初始化,这个时候有两种现象,一种是静态的和thread local storage duration(这四个单词我的理解是整个线程生命周期都存在的对象,全局变量?)一般会置为零初始化;还有一种automatic storage duration(auto和register修饰的,局部变量?)一般是个不确定值(野指针)
零初始化(在内存的表现形式是0?)
- pointers are initialized to null pointer values of their types
- objects of integral types are initialized to unsigned zero
- objects of floating types are initialized to positive zero
- all elements of arrays, all members of structs, and the first members of unions are zero-initialized, recursively, plus all padding bits are initialized to zero
对于Cpp来说呢,有静态初始化和动态初始化一说
Depending on context, the initializer may invoke:
- Value initialization, e.g. std::string s{};
- Direct initialization, e.g. std::string s(“hello”);
- Copy initialization, e.g. std::string s = “hello”;
- List initialization, e.g. std::string s{‘a’, ‘b’, ‘c’};
- Aggregate initialization, e.g. char a[3] = {‘a’, ‘b’};
- Reference initialization, e.g. char& c = a[0];
If no initializer is provided, the rules of default initialization apply.
这里有个default initialization,还有个zero-initialized和constant initialization
默认初始化呢,就当时默认构造函数好了。常量初始化呢,编译时期的。
2 函数
了解函数之前,得先了解语句,毕竟函数不就是把语句打包一下吗?这也是所有编程语言核心之处啊,这里不说编程语言,就说计算机语言,还是蛮简单的。赋值,选择,循环,跳转。
简单不代表容易呀
C/Cpp中选择if和switch,循环for,while和do-while,跳转goto,还有continue,break,空语句,几乎每种编程语言都有,感觉也没啥要注意的,main函数不就是个函数,和y=f(x)
很像啊
函数定义形式如下
1 | 返回值类型 函数名() { |
Cpp的函数比较复杂一点
1 | struct S { |
看到了,一个破函数能有这么多修饰..
返回值
返回值这东西我们知道,一般写在函数名前面,然而Cpp总能给你来点不一样的
1 | string to_string(int a); // 前置返回类型 |
其中auto关键字就表示后置返回类型,后置返回类型的必要性来自于模板函数,因为返回类型依赖于参数
inline和constexpr
在函数声明或定义中函数返回类型前加上关键字inline即把函数指定为内联,函数固定为一个地址
关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用
定义在类声明之中的成员函数将自动地成为内联函数
如果函数体内的代码比较长,使用内联将导致内存消耗代价较高
如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大
面对constexpr,胆子大一点,直接当成常量就ok了
[[noreturn]]
形如[[...]]
被称为属性,属性可以置于Cpp语法的任何位置。[[noreturn]]
放在函数声明语句开始位置,表示我们不希望函数返回任何结果
2.1 参数传递
当程序调用一个函数时,我们为该函数的形参申请内存空间,并用实参初始化形参。参数传递的语义与初始化的语义一致(严格来说是拷贝初始化)。**C中,所有函数参数都是”值传递”;Cpp中,除非形参是引用,其他情况函数参数也都是”值传递”**。所谓值传递,就是传入函数的是实参的副本,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本值。
引用参数
1 | void f(int val, int& ref) { |
当调用函数f时,++val
递增的是实参的副本,++ref
递增的是实参本身
单从拷贝的角度来考虑,如果遇到大对象时,引用传递比值传递更有效。但是此时最好将引用类型的参数声明成const的,表明只是为了效率,而非想修改对象的值
1 | void g(Large& arg); |
对于小对象使用值传递方式;
对于无需修改的大对象使用const引用传递;
如需要返回计算结果,最好使用return而非通过参数修改对象;
使用右值实现移动和转发;
如果找不到合适的对象则传递指针(用nullptr表示没有对象);
除非万不得已,否则不要使用引用传递,传递指针它不香吗?
数组参数
当数组作为参数时,实际传入的是指向该数组首元素的指针。也就是说,当数组作为参数传入函数时,T[]
会转换成T*
,所以也就没有长度一说,如果此时对数组元素赋值,则会改变该数组元素实际值。
1 | // 这叫复合字面量 |
列表参数
一个由{}限定的列表可以作为下述形参的实参
1 类型std::initializer_list<T>
,其中列表的值能隐式地转换成T
2 能用列表中的值初始化的类型
3 T类型数组的引用,其中列表值能隐式地转换成T
1 | template<class T> |
如果存在二义性,则initializer_list参数的函数被优先考虑
数量未定参数
对于某些函数,很难明确指出调用时期望的参数数量和类型,要实现这样的接口:
1 使用可变模板,安全
2 使用initializer_list作为参数类型,安全
3 使用省略号(…)结束参数列表,不安全
默认参数
只能给参数列表中位置靠后的参数提供默认值
1 | int tp1(int a, int b = 1); |
2.2 函数重载
为不同数据类型的同一种操作起同一种名字称为重载。这个概念是Cpp的,C没有。重载发生在一组重载函数集的成员内部,也就是说重载函数应该位于同一个作用域。
自动重载
由编译器决定使用一组函数中的某一个,主要依据实参和哪个函数形参类型最匹配
1 | void print(long l); |
1 精确匹配,无须类型转换或者仅需简单类型转换即可实现匹配
2 执行提升后匹配,执行了整数提升(bool转int,char转int..)
3 执行标准类型转换后实现匹配,比如int转double,double转int,T*
转void*
4 执行用户自定义类型的转换后实现匹配
5 使用函数声明中的省略号进行匹配
手动重载
为了解决自动重载的二义性,方案一,增加一个函数版本;方案二,static_cast
C的重载
C语言真的没法重载了?不过是不能声明同样名字的函数罢了。参考:http://locklessinc.com/articles/overloading/
为什么Cpp可以重载而C不可以,从汇编的角度来看,Cpp汇编完了,总会在函数名字上加点什么,而C就不会
省略号
1 | int add(int a, ...); // 着实有点僵硬,但也重载了一点,个数 |
你说要重载参数类型,我觉得划不来,多写太多代码了
1 | typedef struct { |
还有
1 | float overload_float(float f) |
2.3 指针函数
看完参数,就该瞅瞅返回值了。基础类型的返回值没啥好看的,如果函数返回值是指针,数组,结构体..
1 | // int[] getArr(); // Function cannot return array type 'int []' |
首先,C/Cpp函数不能直接返回一个int[]
,或许是因为不知道大小吧。返回一个结构体似乎是没有问题的,而且貌似还能返回个局部变量。返回指针自然不必说,先不说有个指针函数的概念,我返回个地址咋不行了?
汇编瞅瞅,这次为了搞得更清楚一点呢,换一个新指令gcc -S -fverbose-asm -O0 -m64 RetTest.c
,以”.”开头指令基本是伪指令,可以删了,不用看
汇编的函数跳转过程,参考:linux进程运行空间分析_ww188的专栏-CSDN博客
内存布局参考:https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
1 | .LC0: |
这个鬼程序还是蛮长的,用张图翻译一下
基本上每个颜色就是一个步骤,还是比较容易理解的,所以说,指针函数就这样,没啥难度
再看看,为啥不能返回局部变量
1 | int *getArr() { |
这个程序就不反汇编了,这种情况还是好的,能打印出来数据,更有甚者,程序可能会崩溃。所以永远不要返回局部变量的地址
指针和地址是一回事吗?通常是,但不完全是。我们这里都是用字节去划分内存,但是,如果是用字呢?
2.4 函数指针
之前指针的应用都是指向数据,而函数指针不过就是把指针指向代码。这种指针可以被赋值,存放在数组中,传递给函数以及作为函数的返回值等等。简单的例子
1 | int add(int a, int b) { |
这个程序也没必要汇编了..函数add实际上存在text段的,指针pFun和a, b一样,都在栈中,只是存的值是add的地址,机制就是这么个机制。
还有就是有人喜欢把函数指针和typedef放一块看
1 | int add(int a, int b); |
typedef就是重命名呀。好了,知道这么个机制,我们能做些什么?
函数指针数组
来看一个程序
1 | int create_cmd(int a, int b); |
把函数,指针,数组,三个词放一块还是很有意思的,比如实现一个状态机?
多态
多态是面向对象的概念,C语言是面向过程的语言(我觉得这句话不正确)。C语言就不能实现多态了?且看下面一个没有实质作用的程序
1 | // 抽象类 HUMANITY,基本特性 name,基本方法 intro |
此时是不是发现C语言变得有趣多了?面向对象而已,C语言这么强大的语言必拿下。在这个例子中,不免要思考一件事,向上转型和向下转型?因为子类多余的属性是放在父类后面的,所谓的转型,我们就当是内存截断好了,父类前面的内容必然是符合的,至于后面内容,也不会丢,毕竟还是占内存的。此时再将这个父类向下转型,不过就是扩张而已,恰巧后面的就是子类多出的属性占的内存。
2.5 宏函数
这个没啥好说,#define
这玩意本省就是编译时替换,所以注意多加括号就好了,至于宏的一些技巧,例如拼接,之前也说过了
所以,就到这里了,以后想到啥好玩的,再加进来吧!