Cpp static+extern+inline 学习笔记
整理一下关于C++中的几个关键词(static,extern和inline)的笔记。 static static这个关键词的语义非常复杂,在C语言中本来就有多重语义,C++又添加了额外的语义。 静态函数 默认情况下,函数可以被其它编译单元使用,具有外部链接性,可以使用static将其改为具有内部链接性的静态函数,只有在当前文件中才可以使用此函数。 例如 123static void func();void func(){} 静态全局变量 与静态函数类似,全局变量在默认情况下可以被其它编译单元所使用,具有外部链接性, 可以使用static将其改为静态全局变量,只有在当前文件中才可以使用。 例如 1static int s = 100; 静态局部变量 在函数体内部使用static修饰的局部变量会变成静态局部变量,此时静态的语义不再是对外部不可见,而是延长生命周期,此时变量的存储位置从栈区转移到了数据区,不会因为函数调用的结束而销毁,下次进入函数体时会自动忽略定义和初始化语句,例如 12345int func(){ static int s = 0;...
Cpp 面向对象——访问和继承权限
在 C++ 的面向对象编程中,有三个访问权限限定词:public、private 和 protected,用于定义对类成员(成员包括变量和函数)的可访问性, 也包括在继承后的访问权限变化。 我们主要对class进行讨论,最后会讨论struct和class的区别。 访问权限 C++ 通过访问权限限定词来提供数据封装和信息隐藏机制,这有助于提高代码的可读性和可维护性,三种访问权限限定词的基本语义如下: public成员:公开的类成员,可以通过类的成员函数和类的对象访问。 protected成员:受保护的类成员,只能通过类的成员函数访问,无法通过类的对象访问。 private成员:私有的类成员,只能通过类的成员函数访问,无法通过类的对象访问。 注意这里访问的含义:对数据成员的访问是读写,对成员函数的访问则是函数调用。 对于数据的访问权限并没有被进一步拆分为只读和可读写,只读的访问效果需要基于const实现,实现只写的访问效果则需要考虑左值和右值特性,不在本文的讨论范围。 通过类的对象访问是指a.x和a.show()这类语法。 通过类的成员函数访问是指在成员函数体内部对类的其它成员访...
Cpp 面向对象——虚函数进阶
前面简单介绍了重定义和重写,以及重写所涉及到的虚函数的基本概念,这里整理一下虚函数和动态多态的进阶内容。 C++使用虚函数机制来实现运行期的多态(动态多态),与之相对的是编译期的多态(静态多态), 对于C++来说,静态多态可以通过函数重载和泛型编程实现,而动态多态则通过虚函数实现。 在讨论的范围内,基类或派生类指针/引用的使用方法通常是完全等效的,示例主要以指针的使用为主。 为了简化讨论,我们不关注public,private,protected修饰的区别,无论是对类的方法还是继承关系的修饰,统一使用public。 我们不关注多继承、菱形继承以及虚继承等复杂情景,只考虑简单的单继承情景,不考虑模板类的处理。 虚析构函数 对于含有虚函数的类,非常建议将基类的析构函数也设置为虚函数,此时派生类的析构函数也全部会自动变为虚函数,这可以保证基类指针指向派生类时, 在销毁时可以正确调用派生类的析构函数。 考虑下面的例子 123456789101112131415161718192021222324252627282930#include <iostream>struct Bas...
Cpp 面向对象——重定义与重写、虚函数
在一个作用域中,同名函数但是形参列表不同的函数构成重载的关系, 在考虑面向对象时,则会产生一系列新的问题,因为基类和派生类的作用域是一种非常微妙的关系,既不能说它们是简单的两个独立的作用域,也不能说是一个作用域,而且这种关系是天然不对称的。 在面向对象的语法中对基类和派生类的同名函数(相同或者不同的形参列表)都有着特殊的设计,在本文中我们主要讨论的是不涉及虚函数的部分。 为了简化讨论,我们不关注public,private,protected修饰的区别,无论是对类的方法还是继承关系的修饰,统一使用public。 我们不关注多继承、菱形继承以及虚继承等复杂情景,只考虑简单的单继承情景,不考虑模板类的处理。 重定义(隐藏) 例子 假设基类Base定义了一个方法hello,有好几个版本,相互之间构成重载关系。 派生类Derived如果没有实现hello方法,那么派生类对象仍然是可以直接调用hello方法的,包括基类所实现的各种版本,通过重载决议调用,这是继承所赋予的特点。 例如 12345678910111213141516#include <iostream>struct...
C/Cpp 类型别名
typedef typedef是用于为类型定义别名的关键字,在C和C++中都可以使用。 最简单的用法是对基本类型起别名,例如 1typedef int Length; 下面的是MSVC的stdint.h中的部分源码,对基本数据类型起了意义更明确的别名 12345678typedef signed char int8_t;typedef short int16_t;typedef int int32_t;typedef long long int64_t;typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;typedef unsigned long long uint64_t; 数组类型和指针类型可以通过typedef起别名达到简化语法和提升可读性的目的,例如 12345typedef int IntArray4[4];typedef ...
Cpp 类型转换
关于C++的类型转换的整理笔记,主要关注C++相比于C语言多的部分,而且讨论的范围比较简单,不关注涉及到右值引用的类型转换。 概述 C++基本继承了C语言的类型转换规则,但是对类型转换的处理有很多区别,例如: 对于C语言,隐式转换和显式转换并没有太多区别,仅仅是危险的隐式转换可能会发出警告,使用显式转换会消除警告。 对于C++,安全的隐式转换可以通过编译,但是危险的隐式转换无法通过编译,必须使用显式转换才能顺利编译,C语言风格或C++风格的显式转换均可。 此外,C++相对于C语言还需要考虑很多很多额外的内容,我们将主要关注面向对象的部分,即自定义类型之间的类型转换,以及新加入的四个显式的强制类型转换关键词的使用 const_cast reinterpret_cast static_cast dynamic_cast 现代 C++ 不建议这么做: 避免使用C语言中常用的void *类型,C++提供了更好的工具; 避免使用C语言风格的显式类型转换,可读性太差,不利于后期检查。 现代 C++ 建议这么做: 优先考虑使用static_cast,用它进行一些数值类型的转换还是...
C语言类型转换
关于C语言类型转换的整理笔记。 概述 在C语言中,对于A->B的类型转换过程中,无论是C语言风格的显式转换 (type)value(强制类型转换),还是隐式转换(自动类型转换,编译器自动推断目标类型),只要转换前后的类型一致,它们都遵循相同的转换规则,因此无损转换、截断、取模、UB 等可能的表现都完全一致。 如果A->B的转换过程是非法的,那么无论是显式还是隐式转换,都无法成功编译。 如果A->B的转换过程是合法的,那么都可以顺利编译,但是存在如下细微区别: 如果转换过程较为自然且安全,那么使用隐式转换和显式转换没有任何意义的区别; 如果转换过程较为危险,转换前后可能丢失数据,那么: 使用隐式转换时,编译器或静态代码检查可能会发出警告; 使用显式转换可以抑制这些警告,这表明我们手动确认这个转换是安全的。 注: 隐式转换通常发生在赋值(包括初始化和函数传参)和算术运算中; gcc对于涉及隐式转换的警告比较保守,很多情况下的警告需要使用-Wconversion手动开启。 由于本文就是探究类型转换的细节,因此在示例中更倾向于使用显式转换,以增强代码的可读性...
Cpp 函数参数默认值
C语言不支持函数重载,也不支持函数参数的默认值,这既可以说体现了C语言的简陋,也可以说是避免了很多麻烦。 C++的函数参数支持默认值的机制就比较烦人,因此需要整理一下。 为了提高代码的可读性,C++尽量也不要使用函数默认值,在讨论的最后提供了几种简单的方式可以替代。 基础 C++支持给函数参数提供默认值,例如 12345678910void func(int a, int b=1){ ...}int main(){ func(1,2); func(3); // == func(3,1), a=3,b=1 return 0;} C++要求参数的默认值必须从右向左连续地提供,保证无默认值的参数不能出现在有默认值的参数的右侧,否则编译器无法判断参数缺省时的对应关系,这是语法错误,例如 1234567void func(int a, int b=1, int c){ // compile error ...}void func(int a, int b=1, int c=2){ // ...
Cpp 面向对象——三大特性和 C 语言实现
面向对象编程(OOP)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想。 OOP表现出来的三个最基本的特性就是封装、继承与多态。 在本文中我们将讨论C语言如何简单地实现OOP的基础功能,并且关注C++是如何实现OOP的,将两者进行对比。 对于C++的讨论不涉及过多的语法细节,我们不关注public,private,protected修饰的区别,无论是对类的方法还是继承关系的修饰,统一使用public。 我们不关注多继承、菱形继承以及虚继承等复杂情景,只考虑简单的单继承情景,不考虑模板类的处理。 封装 封装就是在形式上将数据和操作数据的方法打包在一起,然后提供部分接口给外部访问,隐藏内部的实现细节。 但是C语言没有直接提供将内部信息隐藏的机制,我们只能使用其它的间接方案: 君子协定,约定只通过固定的接口进行访问; 利用动态库的符号部分导出的特点将动态库的部分信息隐藏; 利用static变量和函数对源文件外部不可见的性质,将部分信息隐藏 下面我们只关注数据和操作方法的打包,因为C语言的结构体只含有数据成员,我们需要通过函数指针在结构体中加上操作数据的函数,例如 1234...
C语言 数组和指针笔记
虽然在C++中完全不需要处理C语言中原始数组和原始指针等,C++提供了很多更好的替代实现,但是还是顺手整理一下吧。 在本文中的代码会涉及到几个运算符优先级的细节,这里备注一下: 数组取下标([])的优先级高于解引用(*)和取地址(&); 解引用(*)和取地址(&)的优先级相同,连续出现时,按照从右到左结合; 解引用(*)和取地址(&)是互逆的,因此连续出现时(&*和*&)直接抵消。 数组基础 一维数组 定义数组的语法很简单 1int a[5]; 此时变量a的类型是int [5]。 在定义时可以赋初值,此时可以省略数组长度,长度会通过初值个数自动获取,例如 1int a[] = {1,2,3,4,5}; 如果同时提供了数组长度和初值,那么数组长度必须大于等于提供的初值个数(否则报错),此时会对数组元素从前到后赋初值,剩余的元素会被初始化为0,例如 1int a[3] = {1}; // a[0] = 1, a[1] = 0, a[2] = 0 sizeof作用在数组名,返回数组整体占用的字节数,即...