mxxhcm's blog

  • 首页

  • 标签

  • 分类

  • 归档

C++ variable declaration and definition

发表于 2019-11-13 | 更新于 2019-12-17 | 分类于 C/C++

C++类型

C++中,对象(变量)的类型通常有两种:

  1. 基本内置类型,整形和浮点型。整形又包括整数,布尔型,字符串,浮点型又包括单精度浮点型和双精度浮点型,详细介绍可以查看C++算术类型。
  2. 复合类型,包括指针,数组,引用,类,结构,联合类型。

变量和对象

变量

变量是一个有名字的,可供程序操作的存储空间。C++ 中的每个变量都有数据类型,数据类型决定着变量所占内存空间的大小和布局方式,该空间能存储的值的范围,以及变量能够参数的运算。对于C++ 程序员来说,变量和对象一般可以互用。

对象

什么是对象,具有某种数据类型的内存空间,并不区分是复合类型还是内置类型,也不区分是否有名字和是否只读。

变量定义

一条定义语句由一个基本数据类型和紧随其后的一个或者多个声明符组成。每个声明符声明了一个变量并且指定该变量为与基本数据类型有关的某种类型。
基本变量的声明语句由数据类型和变量名组成,声明符就是变量名。而复合类型的声明语句中声明符除了变量名,还会有类型修饰符,如*,&和[]等等。比如引用的声明语句中将声明符写成&d的形式,其中d是声明的变量名,&是类型修饰符。指针是另外一种复合类型,通常将声明符写成*d的形式,其中d是变量名,*是类型修饰符。

1
2
3
int a = 3;
int &b = a;
int *p = &a;

在指针操作中,其中操作符*称为解引用,操作符&称为取地址符。

复合类型的定义

变量的定义包含一个基本数据类型和一组声明符。在一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同,也就是一条定义语句可能定义处不同类型的变量:

1
int i = 1024, *p = &i, &r = i;

在定义语句中,类型修饰符*和&仅仅修饰紧随其后的变量。关于引用和指针的声明,一般有两种写法:

  1. 将修饰符和变量名写在一起,即:

    1
    int *p1, *p2;

  2. 把修饰符和类型名写在一块,即:

    1
    2
    int* p1;
    int* p2;

这两种方法都对,C++ primer采用第一种。

指向指针的指针

声明符中修饰符的个数没限制,可以写很多个。

1
2
3
int ival = 1024;
int *pi = &ival; //pi指向一个int数
int **pi = π //pi指向一个int的指针。

指针的引用

引用不是对象,所以指针不能指向引用。但是指针是对象,所以有指针的引用。复杂的指针或者引用的声明语句,从右往左读。

1
2
3
4
5
6
int i = 42;
int *p;
int *&r = p; //从右往左阅读,r是一个引用,是谁的引用,int*的引用。

r = &i;
*r = 0; // i=0

变量初始化

初始化是在变量创建时给一个特定的值,而赋值是把对象的当前值擦除,使用一个新值代替。

默认初始化

当变量没有指定初值,使用默认初始化的方式进行初始化。默认值由变量类型和变量位置决定。
对于内置类型的变量,如果没有显式初始化,它的位置由定义的位置决定。函数内部的内置类型不会被初始化,定义于任何函数外部的内置类型被初始化为0。未初始化的变量的值是未定义的。
对于自定义的类型来说,每个类决定初始化对象的方式,而且是否允许不经过初始化就定义对象也由自己决定。如果类允许这种行为,由类决定对象的初始值是什么;如果类要求每个对象显示初始化,在创建类对象没有进行明确的初始化操作时,会引发错误。

声明和定义的区别

  1. 声明需要给出变量的类型和名字,但是不需要申请存储空间。
  2. 只进行声明的话使用extern关键字,不要显式的初始化变量。包含了显式初始化的声明就变成了定义。
  3. 变量只能定义一次,但是可以声明多次。可以方便的在多个文件中使用同一个变量。

参考文献

1.《C++ Primer第五版》

C++ type operation

发表于 2019-11-13 | 更新于 2019-12-17 | 分类于 C/C++

类型操作

别名typedef和using

1
2
typdef dobuel wages;    //类型别名
using SI = Sales_imte; //别名声明

指针,常量和类型别名

1
2
3
typedef char *pstring;
const pstring cstr = 0; //基本数据类型是char *,即指针类型
const pstring *p;

不能简单的把pstring用char *替换,如果替换了变成下式:

1
const char *cstr = 0;   //基本数据类型是const char,

很容易把*看成是声明符的一部分,即*cstr的一部分,但是实际上*是和const char在一起的。const char *cstr是指向char常量的指针,而const pstring cstr是指向char的常量指针。

auto关键字

编译器自动分析表达式的类型,auto定义的变量必须有初值。
使用一条auto语句可以声明多个变量,多个变量的基本数据类型必须一样。

1
2
auto i= 0, *p = &i; //正确
auto i = 1, d = 3.14; //错误

复合类型,常量和auto

  1. 编译器使用auto推断出来的值和初始值类型有时候不完全一样。比如使用引用其实使用的是引用对象的值。
  2. auto会忽略顶层const,保留底层const。如果希望auto推断出的是顶层const,需要显式的加一个const,即const auto = ...。
  3. 将引用的类型设为auto也可以保留初始值中的顶层const属性,
  4. 如果给初始值绑定一个引用,并且设为auto类型,这个对象就不是顶层const了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int i = 0, &r = i;
    auto a = r; // a是一个int,而不是int &

    const int ci = i, &cr = ci;
    auto b = ci; //b是一个int
    auto c = cr; //c是一个int, cr是ci别名,ci是顶层const
    auto d = &i; //d是int *
    auto e = &ci; //e是一个指向const的指针。

    const auto f = ci; //f是const int
    auto &g = ci; // g是int &

    const auto &j = 43;//这个j就不是顶层const了,类型是const int &,它是底层const。

decltype类型

decltype和auto的区别:

  1. 它只返回表达式的类型。
  2. 它能识别顶层const和引用类型
    1
    2
    3
    4
    5
    6
    7
    8
    int i = 0, &ri = i, *pi = &i;
    decltype(ri); //是int &
    decltype(ri+0); //是int
    const int ci = 0, &cj = ci;
    decltype(ci) p = 0; //const int,
    decltype(cj) q = 0; //const int &,顶层常量引用可以初始化成字面值
    decltype(ci) x; //错误,常量必须初始化
    decltype(cj) y; //错误,引用必须初始化

引用从来都是作为它所指对象的同义词出现,只有在decltype处是例外。

decltype和引用

  1. 如果表达式的内容是解引用操作,使用decltype将会得到引用类型。
  2. decltype((variable))的结果永远是引用,而decltype(variable)的结果只有在真的是引用的时候才会返回引用。
    1
    2
    3
    int i = 0;
    decltype((i)) d;
    decltype(i) e;

参考文献

1.《C++ Primer第五版》

C/C++ const

发表于 2019-11-13 | 更新于 2019-12-17 | 分类于 C/C++

const类型

  1. const修饰的变量一经初始化,不能改变其值,而且必须进行初始化。
  2. 可以使用任意对象(包含非const对象)初始化const对象。
  3. const对象默认只在当前文件中有效。如果需要在其他文件中使用,在声明和定义的文件中都需要加上extern关键字;而非const对象在定义时不需要使用extern关键字,只需要在其他文件的声明中加上extern即可。
  4. 编译器遇到const变量时,会将所有该变量出现的地方都换成相应的值。

引用和const

可以把应用绑定到const对象上,就像绑定到其他常量上,称为对常量的引用(reference to const),和普通引用的不同在与,对常量的引用不能用作修改它绑定的对象:

1
2
3
4
5
const int ci = 1024;
const int &r1 = ci;

r1 = 42; //这行代码是错的,因为r1是对常量的引用
int &r2 = ci; //这行代码也是错的,因为r2是普通引用

常量引用是对常量的引用的简称。严格来说,并不存在常量引用,因为引用不是一个对象,我们没办法让引用本身恒定不变。但是因为C++中,引用绑定的对象是不能更改的,所以从这层意义上理解,所有的引用又都称得上常量。引用的对象是常量还是非常量决定了引用能进行的操作,但是影响不到引用和对象的绑定关系。

初始化和对常量的引用

之前说过引用的类型对象必须和引用对象的类型一样。但是有两个例外,**一个例外就是初始化常量引用时可以使用任意表达式作为初值。**如:

1
2
3
4
5
int i = 43;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1*2;
int &r4 = r1*2;

根据上面的例子我们也可以看出,对常量的引用可能引用一个非常量对象,我们不能通过r改变i的值,但是我们可以直接改变i的值,因为r和i是绑定的,所以r的值也改变了。

当把一个常量引用绑定到另一种类型上的时候,会发生什么?

1
2
double dval = 3.14;
const int &ri = dval;

上述代码将一个dobule类型的变量绑定到一个const int型引用上,为了让ri绑定一个整数,编译器将上述代码变成了以下形式:

1
2
const int temp = dval;
const int &ri = temp;

将ri绑定到了一个临时对象temp上,但是我们想要ri操作的是dval而不是temp,所以这种行为被判定为非法。

引用和左值,右值

  1. 非常量引用的初始值必须是左值。
  2. 常量引用的初始值可以是右值。比如const int &a = 7;
  3. 常量引用的初始值可以为左值。

指针和const

指针也可以指向常量。类似于对常量的引用(常量引用),有指向常量的指针(pointer to const)。

  1. 之前说过指针指向的类型对象必须和指针的类型一样。但是有两个例外,一个例外就是指向常量的指针可以使用非常量对象的地址进行初始化。
  2. 要想存放指向常量的地址,必须使用指向常量的指针。
  3. 指向常量的指针不能用于改变其所指对象的值,很容易明白,因为指向的是常量,而常量的值是不能修改的,所以指向常量的指针自然不能修改它指向的对象的值。
  4. 那么指向常量的指针的指向能否改变?当然可以了,要不然就不会有const指针了,即指针本就就是常量对象,指向不能改变的指针对象。

可以认为指向常量的指针和对常量的引用都是他们自认为指向了常量对象,而常量对象不能被修改,所以就不能修改所指对象的值。

1
2
3
4
const double pi = 3.14;
double *ptr = π //报错,必须是指向常量的指针
const double *cptr = π //
*cptr = 4; //不能修改常量指针指向的常量的值

const指针

引用不是对象,而指针是对象,因此就像其他对象一样,可以把指针本身定义为常量,叫做常量指针,常量指针必须初始化。
const指针必须初始化,它指向变量的值能否改变只取决于它指向变量的类型。

1
2
3
4
int val = 3;
int *const curErr = &val; //
const double pi = 3.14;
const double *const pip = π //

顶层const

用顶层const表示指针本身是一个常量,用底层const表示指针指向的对象是一个常量。指针可以既是顶层const又是底层const。
更一般的,顶层const可以表示任何的对象是常量,这一点对任意类型都使用。而底层const则和指针,引用等复合类型有关。
可以把一个顶层const赋值给非顶层const对象;但是不能把底层const赋值给非底层const对象,这个多加几句,因为底层const一定是和复合类型指针和引用相关的,所以要把它赋值给一个非底层的const对象,这个待赋值的对象也一定是指针和引用,如果它是非底层const对象,那么就可以通过它修改它指向的对象了,而这个对象是const的,不能被修改。

对常量的引用(常量引用)可以和常量对量绑定;
对常量的引用(常量引用)可以和非常量对量绑定;
非常量引用不可以和常量对象绑定;
非常量引用可以和非常量对象绑定;

指向常量的指针可以指向常量对象;
指向常量的指针可以指向非常量对象;
常量指针可以指向非常量对象;
常量指针不可以指向常量对象;
普通指针不可以指向常量对象;
普通指针可以指向非常量对象;

常量指针(顶层const)可以用来初始化非常量对象。
对常量的引用和指向常量的指针(底层const)不可以用来初始化非常量对象。

constexpr和常量表达式,constexpr函数

  1. 常量表达式,值不会改变,并且在编译过程中就能得到计算结果的表达式。字面值和用常量表达式初始化的const对象也是常量表达式。比如:

    1
    2
    int size = 23;  //不是常量表达式,因为他不是`const`对象
    const int sz = get_size(); //sz是常量,但不是常量表达式,因为它的值需要等到运行时才能获得。

  2. 常量表达式一定是常量,但是常量不一定是常量表达式。

  3. 声明为constexpr的变量由编译器验证它是否是常量表达式。

  4. 声明为constexpr的变量一定是个常量,而且必须用常量表达式初始化。

  5. 如果认定变量一定是个常量表达式,就把它声明成constexpr类型,即constexpr用于声明常量表达式。

  6. constexpr函数的形参和返回值都必须是字面值类型,并且函数体有且只有一条return语句。

  7. constexpr函数不一定返回常量表达式。

字面值类型

算术类型,引用和指针都属于字面值类型,string,IO库和类不属于字面值类型。算术类型包含整形和浮点型,整形中又包含整数,字符和布尔。constexpr只能用于字面值类型。
指针和引用能用定义成constexpr,但是初值受到严格限制,一个constexpr指针的初始值必须是nullptr,0或者某个固定地址的对象。一般来说,函数内部的变量(除了static变量)没有存在固定地址中,而所有函数之外的对象地址固定不变,能用来初始化constexpr指针。constexpr声明的指针是顶层const,即指针本身是个const,它指向的对象不能变,它指向的对象的值能变。

const_cast

  1. const_cast只能改变运算对象的底层const,将常量对象转换成非常量对象,这种性质叫做去掉const性质。如果对象本身不是一个常量,使用强制类型转换获得写权限是一个合法的行为,如果对象是一个常量,使用const_cast执行写操作就会产生未定义的后果。const_cast还可以将一个非const对象变成const对象。

    1
    2
    3
    4
    5
    6
    7
    8
    string s1("hello"); 
    const string s2("world");

    string *p1 = &s1;
    const string *p2 = &s2;

    string *p3 = const_cast<string *>(p2); //去掉底层const,但是通过p3写它指向的东西是未定义行为。
    const string *p4 = const_cast<const string*>(p1); //将非底层const转换成底层const。

  2. 只有const_cast能改变表达式的常量属性,使用其他类型的命名强制类型转换改变表达式的常量属性都会引发编译器的错误,注意不能使用const_cast改变表达式的类型。

  3. 通常用于有函数重载的上下文。

const形参和实参

当形参是const时,必须注意顶层const,顶层const作用于对象本身。当用实参初始化形参时,会忽略掉顶层const,即形参的顶层const被忽略掉了。当形参有顶层const时,传递给它常量或者非常量对象都是可以的。

指针或者引用形参和const

形参的初始化方式和变量的初始化方式是一样的,所以指针或者引用形参和const结合时,按照const变量的初始化规则执行就行。

尽量使用常量引用

把函数不会修改的形参定义成普通的引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。(比如,不能把const对象,字面值或者需要类型转换的对象传递给普通的引用传参)

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

重载和const

重载和const形参

顶层const不影响传入函数的对象

  1. 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
  2. 如果形参是某种类型的指针或引用,即形参是底层const,区分其指向的是常量对象还是非常量对象可以实现函数重载。

const_cast和重载

const_cast在重载函数的情景中有最有用。如下函数:

1
const string& stringCompare(const string &s1, const string &s2);

函数的形参是底层const,可以接收常量或者非常量的实参。但是返回的都是const string的引用,如果输入是两个string的引用,返回一个const string的引用显然是不合理的,这时候就可以使用const_cast了。对上述代码做一个改进:

1
2
3
4
5
6
string& stringCompare(string &s1, string &s2)
{

auto &r = stringCompare(static_cast<const string &>(s1), static_cast<const string &>(s2)a);
return static_cast<string &>(r);
}

参考文献

1.《C++ Primer》第五版
2.https://www.zhihu.com/question/36052573/answer/65756850

C++ compound type reference

发表于 2019-11-13 | 更新于 2020-01-05 | 分类于 C/C++

引用(左值引用)

  1. 引用不是一个对象,没有实际地址,不能定义指向引用的指针。
  2. 引用只是一个别名,必须和一个已经存在的对象绑定在一起,无法更改,必须初始化。
  3. 定义引用时,标识符必须以&开头。
  4. 除了两种特殊情况外,所有引用的类型都必须和它绑定的对象严格匹配。
  5. 引用只能绑定在对象上,不能绑定在字面值或者某个表达式上。(const引用除外)

引用的两种特殊情况

  1. 初始化常量引用时允许用任意的表达式作为初始值,只要该表达式的结果能够转换成应用的类型即可。其次,允许一个常量引用绑定非常量的对象,字面值,甚至是一个表达式。
    1
    2
    double dval = 3.14;
    const int &ri = dval;

编译器把上述代码变成了:

1
2
const inst temp = dval;
const int &ri = temp;

当ri不是常量时,就允许对ri赋值,这样就会改变ri所引用对象的值。但是ri其实是绑定到了一个临时变量temp上,程序员绑定临时变量的目的肯定是想改变dval的值,大家不会想着把引用绑定到临时变量上,然后改变它的值,所以C++就把这种行为归为非法(注:并不是说这种绑定是错误的)。

数组引用形参

参考文献

1.《C++ Primer》第五版中文版

C/C++ compound type pointer

发表于 2019-11-13 | 更新于 2019-12-11 | 分类于 C/C++

指针

  1. 变量的值存储在计算机的内存中,每个变量都占据一个特定的位置,每一个内存位置都由地址唯一确定并引用。指针可以看成是地址的另一个名字2。
  2. 指针变量也是一个变量,其中存放的是另一个变量的地址,因为指针是一个变量,所以指针变量本身也存放在内存中的某个位置2。允许对指针赋值和拷贝,在指针的生命周期内可以先后指向几个不同的对象。
  3. 指针无须在定义时赋值,和其它内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

指针声明和定义

指针也是一个复合类型,需要按照复合类型的声明和定义进行声明。

指针定义

定义指针类型的方法将声明符写成*d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*。即*是修饰声明符的,而不是修饰int的。如下所示:

1
2
3
4
5
6
7
int *p; // 定义一个int*的指针p,p进行了默认初始化
int *a, b, *c; //定义了两个int*类型的指针a,c,一个int类型变量b

char *message = "Hello world!"; //定义一个char*变量,指向数据段的字符串常量区中的字符串"Hello world!"的首字符的地址。
//下面两行代码和上面一行代码的作用是相同的,message是一个`char *`指针,指向字符串常量"Hello world!"的首字符的地址。
char *message = NULL;
message = "Hello world!";

取地址符

指针存放某个对象的地址,要想获取改地址,需要使用取地址符&。除了两种特殊情况外,所有指针类型都要和它所指的对象严格匹配。
两种特殊情况:

  1. 一个指向常量的指针可以指向个非常量对象。
  2. …

指针的值,就是地址,有四种可能取值

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,没有指向任何对象
  4. 无效指针,除了以上三种情况的任何值。

使用无效指针和使用未初始化变量是同类错误,编译器都不负责进行检查。

解引用指针(间接访问)操作符

C11中解引用操作符*的定义:

The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.102)

C++11中解引用操作符*的定义:

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points. If the type of the expression is “pointer to T,” the type of the result is “T.” [ Note: a pointer to an incomplete type (other than cv void) can be dereferenced. The lvalue thus obtained can be used in limited ways (to initialize a reference, for example); this lvalue must not be converted to a prvalue, see 4.1. — end note ]

解引用操作符*应用于指向对象或者指向函数的指针表达式,得到指针指向对象的左值表达式,给解引用的结果赋值其实就是给指针所指的对象赋值([1]2.3.2)。如果不解引用,指针变量中存放的内容就只是地址。
对象(变量)和指针变量中存放的内容(即地址)的区别,对象可以直接进行赋值。指针变量中存放的是一个地址,地址本身就是一个数字,是一个右值,不能对其进行赋值,对这个地址进行解引用,得到指针指向的对象。
定义一个指针p:

1
2
int a=3; 
int *p = &a;

p是int*类型,存放的是变量a的地址,*是间接访问,*p对指针进行解引用得到指针指向对象的左值表达式,其实就是a,&表示取变量a的地址。

未初始化和非法的指针

看一个错误的代码片段:

1
2
int *a; //定义一个指针*p
*a = 12; //把12存储在a指向的内存中,错误

这个代码中犯了一个很严重的错误,我们在声明了变量a,但是没有对它进行显示初始化,所以编译器会对a进行默认初始化,默认初始化并不会为变量分配内存。如果程序执行这个赋值操作,假如a是一个非法地址,程序会出错,终止程序,在UNIX系统上,这个错误称为"segmentation violation"或者"memory fault",它告诉我们程序正在访问一个非法的地址。如果a是一个合法的地址,这就会错误的修改a指向的内存中的值,造成一些难以预料到的错误。

空指针

有以下几种方法声明空指针:

1
2
3
4
5
6
// 方法1.
int *p1 = nullptr;
// 方法2.
int *p2 = 0;
// 方法3.
int *p3 = NULL;//NULL定义在cstdlib中

最好使用nullptr或者0,而避免使用NULL。

void*指针

void *指针可以存放任意类型的地址,但是我们并不知道它存放的是什么类型的对象。

指针的指针

因为指针也是一个变量,所以它自然也就有地址,也就存在指向指针的指针。

1
2
3
4
5
6
7
8
int a, *b, **c;
b = &a;
c = &b; //紧挨着c左边的那个*表示c是一个指针,然后再往左边的那个`*`是和`int`在一起的,表示指针`c`指向的变量的类型是`int *`类型的。
// a,*b, **c表示同样的东西,都是变量a的值。
// *c和b和&a表示同样的东西,都是变量a的地址。
// a表示int类型的变量
// b是a的地址,*b表示对指针p解引用,*b就是a。
// c是b的地址,*c表示对指针c解引用,*c就是b,也就是a的地址,*(*c)也就是*b,也是a。

指针的特点

  1. 存放的是对象的地址,要想获取变量的地址,需要使用取地址符&,访问指针中地址指向的变量,使用解引用符号*,即:

    1
    2
    3
    4
    int val = 32;
    int *p = & val; //指针p存放的是变量val的地址
    int b = *p; //b被初始化为32,p存放的是val的地址,*p获得该地址指向的变量val。
    *p = 3; //将val赋值为3

  2. 赋值永远改变的是=左侧的对象,可以用来判断到底是改变了指针的值还是改变了指针所指的对象的值。

  3. 除了两种特殊情况外,所有指针的类型都必须和它指向的对象严格匹配。

指针和引用的不同点

  1. 指针本身就是一个对象,允许赋值和拷贝,在生命周期内可以指向几个不同的对象;而引用本身并非一个对象,一定定义了引用,它就和一个对象终生绑定在了一起。
  2. 指针定义时无须赋值;而引用必须在定义时赋值。

指针运算

指针加上一个整数的结果是另一个指针。注意,这里假设每种类型都是连续存储的。假设字符型占一个字节,float占四个字节,double占八个字节。
如果将指针加上1:
对于一个字符型,新的指针指向内存中的下一个字符,指针的值实际上增加了1。
对于一个float型,它指向内存中的下一个float,指针的值实际上增加了4。
对于一个double型,它指向内存中的下一个double,指针的值实际上增加了8。
也就是说,对于一个给定类型的指针,将它加一,得到的新指针指向下一个同类型的变量,这也是声明指针类型的作用。

指针的大小

任何类型的指针本身所占的大小都是相等的,取决于计算机的地址大小,如果是32位的地址,指针的大小就是4个字节,如果是64位的地址,指针的大小就是8个字节。!!!这是错误的。。

The size of a pointer depends on many factors - including the CPU architecture, compiler, Operating System etc.
Usually the size is equal to the word size of the underlying processor architecture, and the size of total addressable memory (including virtual memory).
So, for a 32bit computer, the pointer size can be 4 bytes; 64bit computers can have 8 bytes. Or, a 64bit computer running a 32bit OS will have 4 bytes. Still, under a specific architecture, all types of pointers (void*, int*, char*, long* etc) will have same size (except function pointers).
That’s, pointers in C (or C++) doesn’t have a fixed size.

算术运算

C的算术运算只有两种形式。第一种是指针加减一个整数,第二种是两个指针相减。

指针加减一个整数

指针加减一个整数的运算形式只能用于指向数组中某个元素的指针,将它加减一个整数得到的表达式也还是一个指针,它仍指向数组中某个元素。如果对指针进行加法或者减法运算之后,指针所指的位置是在数组第一个元素前面或者在最后一个元素后面,它的效果是未定义的。这种操作编译器不会进行检查,需要程序员自己进行检查。
这种形式也适用于malloc函数动态分配的内存。

指针减指针

两个指针相减的结果类型是ptrdiff_t,是一种有符号整数类型。运算结果是两个指针在内存中的距离,以数组元素的长度为单位,而不是以字节为单位。两个指针必须指向同一个数组,结果可正可负。如果两个指针指向不同的数组,这个距离就没有意思。因为我们不知道两个数组分别存在哪个位置。

关系运算

>, >=, <, <=
关系运算也需要指针指向同一个数组中的元素。为了和C++的迭代器兼容,最好使用==或者!=,因为迭代器不支持关系运算,而指针和迭代器都支持==和!=运算。

C中的指针表达式和左值右值[2]

指针自增自减操作

1
2
char ch = 'a';
char *cp = &ch;
  1. 前置自增操作++cp,将cp的值加一,该操作先将cp的值加一,指向ch后面的一个位置,然后返回cp的一个拷贝。表达式++cp和cp加一后的对象一样。
  2. 后置自增操作cp++,将cp的值加一,该操作先返回对象cp的一个拷贝,然后将cp的值加一,指向ch后面的一个位置。表达式cp++和cp加一前的对象一样。
  3. 解引用前置自增操作*++cp,这个式子其实是对表达式++cp的解引用操作,也就是对cp加一后的拷贝的解引用操作,而不是对cp的操作。
  4. 解引用前置自增操作*cp++,这个式子其实是对表达式cp++的解引用操作,是对cp加一前的拷贝的解引用操作,而不是对cp的操作。

指针表达式和左值右值

关于左值和右值的介绍,可以查看C C++ lvalue and rvalue。
给出下列代码

1
2
3
4
5
6
7
char ch[] = "abc";
char *cp = ch;

//&cp = 4; //这个是错的,&cp是一个地址,但是它本身只是一个数,它的本质和`10=4;`没有区别,使用解引用符号访问这个地址上的对象。
// 10 = 4; 错误,10既不是指针,也不是变量
//*10 = 4; 错误,10既不是指针,也不是变量
*(int*)10 = 4; //10是一个int,首先把它转化成一个指针,表示一个地址,然后使用解引用进行赋值

  1. ch,作为左值时,表示的是ch在内存中的位置;ch作为右值时,值是'a',
  2. &ch,不能当左值,因为它没有存放在内存中;当右值时,值是变量ch的地址
  3. cp,作为左值,是一个指针变量;作为右值,值是变量ch的地址
  4. &cp,无法作为左值;作为右值,值是指针变量cp的地址
  5. *cp,作为左值,和ch等价;作为右值,值是'a'
  6. *cp+1,无法作为左值;作为右值,值是'a'+1。
  7. *(cp+1),作为左值,是一个指针,指向ch后面的一个内存单位;作为右值,是ch后面一个内存单位的值。
  8. ++cp,无法作为左值,右值和*(cp+1)一样。
  9. cp++,无法作为左值,右值和*(cp+1)一样。
  10. *++cp,作为左值,是一个指针,指向ch后面的一个位置;作为右值,是ch后面那个位置的值。
  11. *cp++,作为左值,是一个指针,指向ch;作为右值,'a'。
  12. ++*cp,无法作为左值;作为右值,是'b';
  13. (*cp)++,无法作为左值;作为右值,是'a'。
  14. ++*++cp,无法作为左值;作为右值,是'c'。
  15. ++*cp++,无法作为左值;作为右值,是'c'

指针和数组

  1. 指针和数组的联系很紧密,在很多用到数组名字的时候,编译器都会自动的将它转换成一个指向数组首元素的指针。数组名字是个常量指针。
  2. 使用取地址符获取某个对象的指针,对数组元素使用取地址符就能得到指向该元素的指针。

关于更多指针和数组之间的内容,点击查看数组的介绍。

函数指针

函数指针指向的是函数而不是对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回值和形参共同决定,和函数名无关。例如:

1
bool lengthCompare(const string &, const string &);

这个函数的类型是bool(const string &, const string &),要想声明一个指向该函数的指针,只需要使用指针代替函数名字即可:

1
bool (*pf)(const string &, const string &);

从声明符中的变量名字开始,pf前面有个*,所以pf是个指针,右侧是形参列表,左侧是函数的返回值类型。因此,pf是一个指向函数的指针,函数的参数是两个const string的引用,返回值是bool类型,指针类型是boo(*)(const string &, const string &)

函数指针可以作为形参,也可以作为返回值,还可以使用decltype和typedef简化函数指针。需要注意的是,decltype不会把函数转换成指针。

参考文献

1.《C++ Primer第五版》
2.《C和指针》
3.https://stackoverflow.com/questions/54621080/lvalues-in-the-iso-c11-standard
4.http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1570.pdf
5.https://www.quora.com/What-is-the-size-of-a-pointer-in-C`

C/C++ compound type array

发表于 2019-11-13 | 更新于 2019-12-17 | 分类于 C/C++

数组

数组和vector类似,但是数组是定长的,大小不变,而vector是可变的,可以把vector看成可变长度的数组。
如果不清楚元素的确切个数,使用vector。

定义和初始化

数组的维度必须是确定的,在编译时就知道,即是一个常量表达式(值不会改变并且在编译时就已知)。
默认情况下,数组的元素被默认初始化。和内置类型的变量一样,如果在数组内部定义了某种内置类型的数组,那么默认初始化得到的数组含有未定义的值。

显式的初始化数组

  1. 可以忽略维度,编译器会根据初始值推测出来;
  2. 如果指明维度,初始值的数量不应该超出指定的大小;
  3. 如果维度比提供的初始值大,其它的元素被初始化成默认值(值初始化)。

数组不支持直接拷贝和直接赋值

不能将数组或的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值。

数组和vector

不允许使用一个数组为另一个数组赋初值,也不允许使用vector初始化数组。但是支持使用数组来初始化vector:

1
2
int arr[] = {1, 2, 3};
vector<int> vi(begin(arr), end(arr));

复杂数组的声明

对于数组声明的理解,从变量名开始,先往右,再往左读。比如:

1
2
3
4
5
6
int arr[10];    //数组arr,有10个元素,每个元素是int类型
int *ptrs[10]; //数组ptrs,有10个int*类型元素,
int &refs[10]; //错误
int (*Parray)[10] = &arr; //数组的指针Parray,指向一个有10个int类型的数组
int (&arrRef)[10] = arr; //数组的引用arrRef,引用一个10个int类型的数组
int *(&array)[10] = ptrs; //数组的引用array,引用一个10个int*类型的数组

访问数组元素

  1. 范围for 语句。
  2. 下标运算符。下标的类型是size_t类型。它是一种机器相关的无符号类型,足够大能够表示内存中任意对象的大小。定义在<stddef.c>或者<cstddef>头文件中。

数组和指针

数组名和指针

  1. 一般情况下,可以使用取地址符获取某个对象的指针。数组元素也是对象,使用下标运算符得到数组指定位置的对象,使用取地址符就能得到指向该元素的指针。([1]3.5.3)
  2. 数组的另一个特性是,在大多用到数组名字的时候,编译器都会自动的将它转换成一个指向数组首元素的指针,而且数组名是一个常量指针([1]3.5.3)。或者说数组作为右值时,编译器会自动将数组转化成数组首元素的地址([2)]。
  3. 在C中,只有两种操作不把数组名字当做常量指针,当数组名字用作sizeof和&的操作对象时。当sizeof的输入是数组名时,返回的是数组的长度(字节为单位)。而&对数组名取地址时,返回的是指向数组的指针。

在C++复合类型指针中介绍了指针的算术运算。指针的算术运算有一个要求,就是指针必须指向数组中某一个元素。如下所示:

1
2
3
4
5
6
7
8
const size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip;
ip = arr; //这个不是数组的赋值。事实上,这是指针的赋值。
int *ip2 = ip+4;

int *p = arr + sz;
int *p2 = arr + 10;

上述代码中,给arr加上sz时,编译器自动的将它转换成指向数组arr中首元素的指针。执行加法操作,其实就是指针的算术运算,最后指向arr的第五个元素之后的位置。如果超出了这个位置,就会出错,编译器不会检查这种错误。

下标引用

  1. 对于内置数组来说,执行下标运算,相当于先将数组转换为首元素的指针,然后执行数组的算术运算。如下所示:
    1
    2
    3
    4
    5
    int ia[] = {0, 2, 4, 6, 8};
    int i = ia[2]; //这行代码其实相当于以下两行代码

    int *p = ia;
    i = *(p+2);

除了优先级外,下标运算和间接访问(解引用)完全一样。

  1. 而且只要指针指向的是数组中的元素或者数组中尾元素的下一个位置,都可以执行下标操作。当指针指向数组尾后元素时,该位置不可访问!但是可以访问其他位置。

    1
    2
    3
    int *p = &ia[2];
    int j = p[1];
    int k = p[-2];

  2. 和标准库string,vector下标操作的不同之处在于,内置数组的下标操作中,,下标可以是负的,而标准string,vector中下标必须是无符号类型。

数组!=指针

需要注意的是,数组和指针并不是等价的!考虑以下代码:

1
2
int a[5];
int *b;

a和b不能互换使用,即使它们都可以使用指针运算,可以进行间接访问和下标引用操作。它们并不一样。
定义一个数组时,编译器会根据声明指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置。定义一个指针变量时,编译器只为指针本身保留内存空间。所以,进行了上述定义以后,*a合法,而*b不合法。

C++中的数组和指针

  1. 使用decltype关键字对数组进行解析时,不会将数组转换成指针,它会将识别出数组类型。使用auto分析数组类型时会推断出指针类型。
  2. 指针也是迭代器。通过数组名字或者数组首元素地址都可以得到指向数组第一个元素的指针。
  3. begin和end获取数组首元素指针和尾后指针。尾后指针不能执行解引用和递增操作。

C风格字符串

C风格字符串不是一种类型,而是一种约定俗成的写法。按照约定,C风格字符串存放在数组中,并且以空字符'\0'结束。关于C风格字符串更多的内容可以查看。

字符数组

  1. 使用列表初始化。

    1
    2
    char str2[] = {'h', 'e', 'l', 'l', 'o'};
    char str3[] = {'h', 'e', 'l', 'l', 'o', '\0'};

  2. 使用字符串字面值初始化。

    1
    2
    char str1[] = "helloworld";
    //编译器会隐式的在最后加一个"\0",sizeof(str)会计算这个"\0", strlen(str)不会

这种方式是字符数组初始化的简便写法。

  1. 指针和C风格字符串
    1
    char *messages= "hello world";

对于以上三种方式来说,方式1和2是等价的,这种方式中的"helloworld"存放在栈中。而第三种有些特殊,在第三种方式中,"hello world"是一个字符串字面值常量,存放在数据区的字符串常量部分[6,7,8,9]。事实上,它是一个常量字符数组,是一个不可修改的左值[9],把它作为右值时,会进行类型转换将左值转换成右值,即使用常量字符数组首字符的地址进行初始化。

C风格字符串操作函数

C语言标准库<string.h>或者C++版本的<string.h>提供了以下的字符串操作函数,它们的参数必须是指向以空字符结束的字符数组的指针。在函数内存不会验证这些字符串参数是否满足要求。

  • strlen(p),返回p指向的字符串的长度,不包括空字符
  • strcmp(p1, p2),p1==p2,返回0,p1>p2,返回正值,否则返回负值。
  • strcat(p1, p2),p2拼接到p1,返回p1
  • sctcpy(p1, p2),p2拷贝到p1,返回p1

有一点需要注意的是,p2必须能够容纳下拼接后或者拷贝后的字符串,编译器不会进行检查,这需要由程序员自己进行检查。

C风格字符串的比较

两个C风格字符串的比较,其实比较的是指针而不是字符串本身。如下所示:

1
2
3
char str1[] = "hello";
char str2[] = "world";
if(str1 < str2) //这行代码比较的不是两个字符串,而是两个指针。

string和C风格字符串的相互转换

  1. 允许使用以空字符结束的字符数组初始化string对象或者为string对象赋值。
  2. string对象的加法运算中允许使用空字符结束的字符数组作为其运算对象,不能两个都是。
  3. string对象的复合赋值运算中允许使用以空字符结束的字符数组作为其右侧运算对象。

string转换成C风格字符串

1
2
string s("hello world!");
const char *str = s.c_str();

多维数组

多维数组指的是数组的数组。多维数组的定义方式如下:

1
2
int ia[3][4];   //大小为3的数组,每个元素是一个大小为4的数组
int arr[10][20][30]; //大小为10的数组,每个元素是大小为20的数组,每个数组的元素又是一个大小为30的数组。

存储顺序

按照从右到左的维度顺序依次进行存储,实际上它们存储在了线性空间内。比如对于一个数组int array[3][6],它在内存中的存储顺序其实是:
a[0][0], a[0][1], a[0][2], a[0][3], a[0][4], a[0][5], a[1][0], a[1][1], a[1][2], a[1][3], a[1][4], a[1][5], a[2][0], a[2][1], a[2][2], a[2][3], a[2][4], a[2][5]。

多维数组的初始化

使用花括号括起来的一组值初始化多维数组。

  1. 指定所有元素的值。

    1
    int a[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

  2. 初始化一部分,其余的进行值初始化

    1
    int a[3][4] = {0, 1, 2, 3};

只对a[0]进行了初始化。

  1. 初始化每一行的部分元素。
    1
    int a[3][4] = {{0}, {1}, {2}, {3}};

只对每一行的第一个值进行了初始化。

多维数组数组名和指针

一维数组的数组名是一个指针,它的类型是指向元素类型的指针。多维数组也一样,多维数组的数组名也是一个指针,只不过它的类型是指向数组的指针。比如:

1
int matrix[3][10];

数组名matrix实际上是指向数组首元素的一个指针,数组首元素是一个int [10]类型的数组,所以matrix是一个指向int [10]类型数组的指针,解引用操作*matrix得到一个int [10]类型的数组,这个东西其实也是一个数组名,相当于一个指针,指向一个int类型。*(*(matrix+1)+2)其实就是matrix[1][2]。

多维数组的下标

如果要访问多维数组中的某个元素,必须指定所有维度的下标。即:

1
2
3
int matrix[3][10];
//要访问第2行的第16个元素,使用下标[1][15]。
matrix[1][15];

但是,实际上,下标和间接引用是等价的,在多维数组中也一样。数组名matrix可以当成一个指针,matrix +1实际上指向第二个int [10]的数组,*(matrix+1)是一个int [10]的数组。

指向数组的指针

在下面的代码中,pa是一个int指针,p是一个int [10]类型的指针,指向matrix的第一行的10个元素。

1
2
int array[10], *pa = vector;
int matrix[3][10], (*p)[10] = matrix;

指针数组

我们可以创建指针的数组。例如:

1
int *api[10];

创建了一个数组,数组有10个元素,每个元素都是一个int*类型的指针。可以创建一个char*类型的指针数组:

1
2
3
4
5
6
7
8
9
const char *keyword[] = {
"do",
"while",
"if",
"for",
"return",
"switch",
NULL
}

可以参考下面示意图:
char_pointer_array

数组和左值

数组本身是一个不可修改的左值。

字符串字面值和左值

字符串字面值常量是一个常量,不可修改的左值,它以数组的形式存储。

为什么数组不支持赋值

  1. 不支持数组的拷贝是为了避免不必要的复制开销,数组复制将会导致连续的内存读和写。
  2. 为什么用指针代替数组,不是因为他们太像了,而是避免赋值的开销,因为c里面只有值传递,如果对数组采用pass by value,会有很大的开销。

array,&array和&array[0]

array和&array[0]是一样的,它们和&array有什么区别呢?[3]

1
2
3
4
int array[] = {1, 2, 3, 4,5};
printf("array=%p, &array=%p\n", array, &array);

printf("array+1=%p: &array+1=%p\n", array+1, &array+1);

程序的输出如下:

1
2
array=0x7ffc50c541c0: &array=0x7ffc50c541c0
array+1=0x7ffc50c541c4: &array+1=0x7ffc50c541d4

根据上面程序的输出,array和&array得到了一样的地址。但是它们并不是一样的!!!它们的地址相同,但是地址的类型不同。
对指针进行算术运算,将array和&array都加一,我们却得到了不同的结果。事实上,array是指向数组第一个元素的指针,而&array是指向整个int [5]数组的指针。因此,根据指针运算规则,对地址array和&array进行算术运算,得到了不同的结果。

数组和函数

数组形参

数组有两个特殊的性质:

  1. 不允许拷贝,因为不能拷贝数组,所以不能以值传递的方式使用数组参数。
  2. 在使用数组时通常会将其转换成指针。因为数组会被转换成指针,所以为函数传递数组时,实际上传递的是指向数组首元素的指针,这样子可以节约开销。

管理数组转换的指针

当传递给函数一个数组时,实参自动的转成指向数组首元素的指针,数组的大小对于函数的调用没有什么影响。因为数组是以指针的形式传递给函数的,所以函数其实是不知道数组的大小的,调用者应该为此提供一些额外的信息。通常有三种方式:

  1. 显示传递一个表示数组大小的形参
  2. 使用标记指定数组长度,要求数组本身包含一个结束标记,典型的例子是C风格字符串。
  3. 使用标准库规范,传递数组首元素和尾后元素的指针。可以使用begin和end函数获得数组的首元素和尾后元素的指针。

数组形参和const

当函数不需要对数组进行写操作时,数组形参应该是指向const的指针。只有当函数确实需要改变数组元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

C++允许将变量定义成数组的引用,形参也可以是数组的引用。此时,引用形参绑定到数组上。

传递多维数组

C++其实没有真正的多维数组,多维数组其实就是数组的数组。把多维数组传递给函数时,传递的是指向数组首元素的指针。而多维数组是数组的数组,首元素本身就是一个数组,多维数组转换成指向数组的指针。数组第二维以及后面维度都是数组类型的一部分,不能省略。

函数返回值和数组

因为数组不能被拷贝,所以函数不能返回数组。不过可以返回数组的指针或者引用。

声明一个返回数组指针的函数

返回数组指针的函数形式如下所示:

1
Type (*function(parameter_list)) [dimension]

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小,(*function(parameter_list))两端的括号必须在,否则函数的返回类型就是指针的数组。如下示例:

1
int (*func(int i))[10];

func带有参数,说明它是一个函数,前面带有解引用操作,说明可以对函数调用的结果执行解引用操作,括号右面说明这是一个维度为10的数组,括号左面是数组类型。

使用尾置返回类型

可以使用尾置返回类型,任何函数的定义都能使用尾置返回,但是这种形式一般用于比较复杂的返回类型,比如数组的指针或者数组的引用。形式如下:

1
auto func() -> int (*)[10];

使用decltype

可以使用decltype声明返回值类型。比如返回一个指针时,

1
2
3
4
5
6
7
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8, 10};

decltype(odd) *func(int i)
{
return (i%2)? &odd: &even;
}

参考文献

1.《C++ Primer第五版》
2.https://stackoverflow.com/questions/1641957/is-an-array-name-a-pointer
3.https://www.geeksforgeeks.org/whats-difference-between-array-and-array-for-int-array5/
4.https://stackoverflow.com/questions/3437110/why-do-c-and-c-support-memberwise-assignment-of-arrays-within-structs-but-not/3439969
5.https://stackoverflow.com/questions/45656162/why-cant-a-modifiable-lvalue-have-an-array-type
6.https://stackoverflow.com/questions/1704407/what-is-the-difference-between-char-s-and-char-s
7.https://stackoverflow.com/questions/2938895/difference-between-char-a-string-char-p-string
8.http://c-faq.com/decl/strlitinit.html
9.https://stackoverflow.com/questions/10004511/why-are-string-literals-l-value-while-all-other-literals-are-r-value

C getline vs C++ getline

发表于 2019-11-13 | 更新于 2019-12-17 | 分类于 C/C++

getline in C

原型

使用man getline可以看到UNIX提供的库函数getline的原型。

1
2
3
4
#include <stdio.h>

ssize_t getline(char **lineptr, size_t *n, FILE *stream);
ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream);

性质

  1. getline()从stream中读入一整行,
  2. 如果*lineptr设置为NULL并且*n=0,getline()会分配一个buffer存储读入的line。这个buffer应该被用户程序释放,即使geline()失败了。
  3. 如果*lineptr包含一个指针,大小是*n字节。当buffer不能存下读入的line时,getline()会使用realloc(3)对buffer进行resize,更新*lineptr和*n。
  4. 只要成功调用,*lineptr和*n分别表示的是buffer的地址和分配的内存大小。
  5. getdelim()和getline()一样,只不过可以指定一个delimiter而不是使用newline作为delimiter。任何delimiter都会存进*lineptr中。
  6. 为什么getline需要的是char**而不是char*,因为getline在lineptr指向的空间不足时,重新分配内存,如果使用的是char*的话,当getline重新分配内存后,我们就失去了对line的访问,而使用一个char**类型的字符串,使用一个char**类型,即指针的指针记录每次分配的char*。[3]。

getline in C++

cin.getline

cin.getline()是操作C strings,即字符数组的。不会将'\n'读入。

std::getline

  1. std::getline()是操作C++ strings的,即std::string。
  2. std::getline()从input stream中读一个string,遇到delimiter就停止,默认的delimiter是'\n'。即使输入开始就是delimiter也会停止。
  3. std::getline()会把delimiter也读进来,然后把读到的内容存到string对象中去,存入的内容不包含delimiter。

代码示例

getline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char *argv[])
{
FILE *stream = stdin;
char *line = NULL;
size_t len = 0;
ssize_t nread;

while ((nread = getline(&line, &len, stream)) != -1) {
printf("Retrieved line of length %zu:\n", nread);
fwrite(line, nread, 1, stdout);
}

free(line);
exit(EXIT_SUCCESS);
}

getdelim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char *argv[])
{
FILE *stream = stdin;
char *line = NULL;
size_t len = 0;
ssize_t nread;
int delim = ',';

while ((nread = getdelim(&line, &len, delim, stream)) != -1) {
printf("Retrieved line of length %zu:\n", nread);
fwrite(line, nread, 1, stdout);
}

free(line);
exit(EXIT_SUCCESS);
}

参考文献

1.https://stackoverflow.com/questions/4872361/why-are-there-two-different-getline-functions-if-indeed-there-are
2.https://www.reddit.com/r/learnprogramming/comments/4fx64h/is_there_a_difference_between_cingetline_and/
3.https://stackoverflow.com/questions/5744393/why-is-the-first-argument-of-getline-a-pointer-to-pointer-char-instead-of-c/36098042

C/C++ exit and return

发表于 2019-11-12 | 更新于 2020-02-23 | 分类于 C/C++

进程终止

总共有八种方式可以让进程终止,包括五种正常终止和三种异常终止,前五种是正常终止,后五种是异常终止:

  1. 从main返回,相当于调用exit。
  2. 调用exit,ISO C定义的,它的操作包括调用各个exit handler,处理所有标准I/O流。
  3. 调用_exit或者_Exit,ISO C定义了_Exit,而POSIX.1说明了_exit。它的目的是提供一种无需运行exit handler或者信号处理程序而终止的方法。是否对标准I/O流进行flush,取决于实现。在UNIX中,_Exit和_exit是同义的,并不flush I/O流。
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用pthread_exit
  6. 调用abort
  7. 接到一个signal
  8. 最后一个线程对取消请求做出响应

不管进程以哪种方式终止,最后都会执行内核中的同一段代码,这段代码为相关进程关闭所有打开的文件描述符,释放它使用的内存。
为了让终止进程能够通知父进程它是如何终止的。对于3个终止函数(exit, _exit, _Exit),将它们的exit status作为参数传递给函数。在异常终止的情况下,内核产生一个指示其异常终止原因的terminaiton status(终止状态)。在任意终止情况下,这个终止进程的父进程都能用wait或者waitpid函数获得它的终止状态。
如果父进程在子进程之前终止,那么对于父进程终止的所有进程,它们的父进程都变成init进程,终止状态返回到init进程。具体是怎么操作:对于一个即将终止的进程,内核检查所有活动进程,判断其中是否有待终止进程的子进程,如果有的话,将这些进程的父进程的ID改为init进程的ID 1。
如果子进程在父进程之前终止。内核为每一个终止进程保留了一部分信息,当终止进程的父进程调用wait或者waitpid时,可以获取这些信息,这些信息包含终止进程PID,进程的终止状态,进程占用的CPU时间总量。内核可以释放这些进程的内存,关闭打开的文件。如果一个进程终止了,但是它的父进程没有等待它,它被称为一个zombie(僵尸)进程。如果一个长期运行的进程,fork了很多子进程,除非父进程等到取得子进程的终止状态,要不它们就会变成僵尸进程。当父进程结束时,僵尸进程就会结束???将所有的子进程交给init进程?但是这些进程都已经结束了。
init的子进程,不会变成僵尸进程,因为init进程被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数获得其终止状态。

exit和_Exit函数

C11标准定义

exit定义在<stdlib.h>头文件中

void exit( int exit_code ); (until C11)
_Noreturn void exit( int exit_code );(since C11)
Causes normal program termination to occur.
Several cleanup steps are performed:

  • functions passed to atexit are called, in reverse order of registration(调用atexit注册的函数)
  • all C streams are flushed and closed (冲洗C的缓冲区,不是不关闭流吗???)
  • files created by tmpfile are removed (删除临时文件)
  • control is returned to the host environment. If exit_code is zero or EXIT_SUCCESS, an implementation-defined status, indicating successful termination is returned. If exit_code is EXIT_FAILURE, an implementation-defined status, indicating unsuccessful termination is returned. In other cases implementation-defined status value is returned.(将控制权返还给操作系统。)

-Exit定义在<stdlib.h>头文件中

void _Exit( int exit_code ); (since C99) (until C11)
_Noreturn void _Exit( int exit_code );(since C11)
Causes normal program termination to occur without completely cleaning the resources.
Functions passed to at_quick_exit() or atexit() are not called. Whether open streams with unwritten buffered data are flushed, open streams are closed, or temporary files are removed is implementation-defined. (不调用atexit注册的函数,是否冲洗缓冲区,关闭打开的stream和删除临时文件是由实现定义的,UNIX都不做这些操作)
If exit_code is 0 or EXIT_SUCCESS, an implementation-defined status indicating successful termination is returned to the host environment. If exit_code is EXIT_FAILURE, an implementation-defined status, indicating unsuccessful termination, is returned. In other cases an implementation-defined status value is returned.

性质

  1. exit和_Exit是ISO C的内容,而_exit是POSIX.1的内容。
  2. 它们都用于正常终止一个程序,_Exit和_exit立刻进入内核,_Exit和_exit是否冲洗缓冲区是由实现定义的,UNIX上选择不冲洗。而exit先执行一些清理操作,然后返回内核,exit函数首先调用atexit函数登记的终止处理程序,然后冲洗标准I/O流,现代的exit实现都不会关闭标准I/O流,之前的一些实现还会关闭标准I/O流,这在调用vfork的时候可能会出现问题,还会删除临时文件。
  3. 三个退出函数都需要一个整形的参数,被称为exit status。
  4. 如果满足以下条件:
    • 调用这三个函数不带终止状态
    • main执行了一个不带返回值的return语句
    • main没有声明返回类型为整形,进程的终止状态是未定义的。
      那么这个进程的终止状态是未定义的。
  5. main返回返回一个整型值和用该值调用exit是等价的。对于某些C编译器和UNIX lint(1)程序来说,会产生警告信息,因为这些编译器并不了解main中的return和exit的作用是相同的。避开这种警告信息的一种方法是在main中使用return而不是exit,这样做的结果是UNIX grep命令无法找出程序中所有的exit调用。另一个方法是将main声明为void而不是int,然后调用exit,但是这不并不是标准,ISO C和POSIX.1定义main的返回值应当是带符号整形。

函数原型

1
2
3
4
5
6
7
8
#include <stdlib.h>

void exit(int status);
void _Exit(int status);

#include <unistd.h>

void _exit(int status);

atexit

C11标准定义

Registers the function pointed to by func to be called on normal program termination (via exit() or returning from main()). The functions will be called in reverse order they were registered, i.e. the function registered last will be executed first.
The same function may be registered more than once.
atexit is thread-safe: calling the function from several threads does not induce a data race.
The implementation is guaranteed to support the registration of at least 32 functions. The exact limit is implementation-defined.

性质

每个进程可以通过atexit register至多32个由exit自动调用的函数,这些函数被称为exit handler(终止处理程序)。

1
2
3
#include <stdlib.h>

int atexit(void (*function)(void));

  1. atexit的参数是一个函数地址,不会有返回值
  2. exit调用atexit register的程序的顺序和使用atexit进行register的顺序相反。
  3. ISO C和POSIX.1标准规定,exit首先调用各个exit handler,然后使用fclose关闭所有标准I/O流。
  4. POSIX.1对ISO C进行了扩展,如果程序调用了任何exec函数,清除exit handler。
  5. 内核执行一个程序的唯一方法是调用一个exec函数。进程自愿终止的唯一办法是显式或者隐式的(通过exit)调用_exit和_Exit。

return和exit

  1. return是C/C++语言的关键字,是语言级别的;而exit()是一个函数,它是对系统调用_exit()的封装,是系统调用层次的。
  2. return结束一个函数的执行;而exit()结束一个进程,删除进程使用的内存空间,并且将应用程序的一个状态返回给OS,这个状态标识了进程的运行信息。
  3. exit(0)表示正常运行程序并退出程序,exit(1)表示非正常运行导致退出程序;return 0和retrun 1能够起类似的作用。
  4. 对于我们自定的函数,可以return给操作系统,交给相关的处理程序调用exit或者程序自身直接调用exit。

C++中的区别

在C++中,退出程序时,exit并不会调用局部非静态对象的析构函数,而return会调用局部非静态对象的析构函数。

参考文献

1.https://stackoverflow.com/questions/461449/return-statement-vs-exit-in-main
2.https://www.geeksforgeeks.org/return-statement-vs-exit-in-main/
3.https://www.zhihu.com/question/26591968/answer/33839473
4.https://www.zhihu.com/question/26591968/answer/33330774

UNIX system call vs library call

发表于 2019-11-12 | 更新于 2019-12-17 | 分类于 UNIX

系统调用(system call)和库函数(library function)

系统调用

所有的操作系统都提供多种服务的入口点,通过这些入口点向内核请求服务,这些入口点被称为系统调用(system call)。系统调用处于kernel mode,一些任务只能在kernel mode运行。比如和硬件的交互,系统调用使得用户mode的进程可以通过系统调用进入kernel mode,从而实现和硬件的交互。。
系统调用接口可以在man的第二部分查看,它是用C语言定义的,与具体系统如何调用一个系统调用的实现技术无关。这些和早期的操作系统按照传统方式用机器的汇编语言定义内核入口点。
UNIX使用的方法是为每个系统调用在标准C库中设置一个同名函数。用户进程使用标准C调用相应的函数,这些函数又根据系统调用调用相应的内核服务。

库函数

库函数可以在man手册的第三部分查看,第三部分定义了程序员可以使用的通用库函数。库函数可以调用系统调用,也可以不调用系统调用,比如read函数会调用系统调用,而atoi等并不使用任何系统调用。

系统调用和库函数之间的关系

  1. 从实现角度来看,系统调用和库函数有着根本的区别,系统调用处于内核mode,库函数属于用户mode。
  2. 从用户应用角度考虑,可以把系统调用看做C函数,使用系统调用还是库函数不重要,它们都是为应用程序提供服务的。
  3. C函数只是系统调用和库函数的一种实现,系统调用和库函数都可以以其他方式实现。
  4. 系统调用通常只是提供一种最小接口,而库函数实现更复杂的功能。
  5. 库函数可以被替换,但是系统调用通常是不能替换的。
  6. 库函数可以调用系统调用,也可以不调用系统调用。
  7. 应用程序既可以调用库函数也可以调用系统调用。
  8. 进程控制系统调用(fork, exec和wait)等通常由应用程序直接调用。为了简化一些常见情况,UNIX也提供了一些库函数,如system和popen。
  9. 库函数链接到用户程序,在user space执行,而syste call没有链接到用户程序,在kernel space执行
  10. 库函数的执行时间被计算为user level time,而system call的执行事件算作system time的一部分。
  11. 库函数可以简单的进行debug,而系统调用不能debug,因为它们被kernel执行。

对于第4条,可以考虑以下例子:
sbrk(2)是分配存储空间的UNIX系统调用,它按照指定字节数增加或者减少进程地址空间。如何管理进程的地址空间由进程决定。
malloc(3)是公用函数库中的一个存储分配空间函数,它负责进行进程的存储地址管理。
我们可以自己实现一个malloc,但是它很有可能还要使用sbrk(2)。内核中的系统调用ssbrk是系统层面的空间分配,而库函数malloc是在用户层面进行操作。

参考文献

1.《APUE》第三版
2.https://www.thegeekstuff.com/2012/07/system-calls-library-functions/
3.https://stackoverflow.com/questions/29816791/what-is-the-difference-between-system-call-and-library-call
4.https://unix.stackexchange.com/questions/6931/what-is-the-difference-between-a-library-call-and-a-system-call-in-linux
5.https://unix.stackexchange.com/questions/57232/difference-between-system-calls-and-library-functions

C/C++ main argc argv

发表于 2019-11-12 | 更新于 2019-12-17 | 分类于 C/C++

main函数

main函数是C语言和C++ 程序的入口,C和C++ 的标准要求它们的实现必须支持以下两种形式:

1
2
int main(void) {...}
int main(int argc, char* argv[]){...}

任何C和C++ 库实现都必须实现以上两种形式的main,除此以外,可以根据标准进行其他扩展实现,但是这样子可能在一个平台上能运行的程序在另一个平台上不能运行,即除了标准的两种main,其他扩展都是不可移植的。
需要注意的一点是,C和C++ 标准对于main的扩展有要求,C++ 标准要求所有的main都必须返回int类型,只有它们的参数可以改变。而C要自由一些,可以有void main(char, dobule)的实现,但是在C++ 标准中这是不支持的。int main(int ,char*, char**)在C和C++ 标准中都是允许的。

main函数示例

假设有一个名为prog的可执行文件,其中包含一个main函数,可以通过命令行选项向程序传递参数:

1
prog -d -o file data0

这些参数通过两个形参argc和argv传递给main函数。形参argv是一个数组,数组元素是字符串指针,即argv是一个C风格字符串指针数组。而argc表示的是字符串数组的长度。
当一个实参传递给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次存放命令行提供的实参。最后一个指针指向的元素应该保证为0。
注意,当使用argv中的实参时,注意argv[0]保存的是程序名字或者空字符串,可选的实参从argv[1]开始。

参考文献

1.《C++ Primer第五版》
2.https://stackoverflow.com/questions/9554513/c-main-vs-c-main
3.《你必须知道的495个C语言问题》

1…8910…34
马晓鑫爱马荟荟

马晓鑫爱马荟荟

记录硕士三年自己的积累

337 日志
26 分类
77 标签
RSS
GitHub E-Mail
© 2022 马晓鑫爱马荟荟
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.6.0