Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C/C++ 相关 #99

Open
ShannonChenCHN opened this issue Dec 20, 2017 · 19 comments
Open

C/C++ 相关 #99

ShannonChenCHN opened this issue Dec 20, 2017 · 19 comments

Comments

@ShannonChenCHN
Copy link
Owner

ShannonChenCHN commented Dec 20, 2017

No description provided.

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Feb 18, 2018

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 24, 2018

函数指针

定义:指向函数的指针变量。

C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

函数指针有两个用途:调用函数和做函数的参数。

函数指针类型变量的定义:

return_type (* var_name)(args)

例子:

#include<stdio.h>
int max(int x,int y){ 
   return (x>y? x:y);
}
int main(){   
      int (*ptr)(int, int);   
      int a, b, c;   
      ptr = max;    
      scanf("%d%d", &a, &b);  
      c = (*ptr)(a,b);   
      printf("a=%d, b=%d, max=%d", a, b, c);   
      return 0;
}

函数指针和指针函数的区别:

函数指针是指一个指向函数入口地址的指针,而指针函数是一个返回值为指针类型的函数。前者是指针,后者是函数。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 24, 2018

@ShannonChenCHN
Copy link
Owner Author

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 26, 2018

二维数组

二维数组本质上是以数组作为数组元素的数组,即“数组的数组”。

二维数组的定义:

类型说明符 数组名[常量表达式][常量表达式];
int a[3][5];   // 定义一个二维数组(3行5列)

二维数组又称为矩阵,行列数相等的矩阵称变方阵。对称矩阵:a[i][j] = a[j][i];对角矩阵:n阶方阵主对角线外都是零元素。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 26, 2018

char 类型,字符和字符串

字符数组实际上是一系列字符的集合,也就是字符串(String)。在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。

char a[10];  //一维字符数组
char b[5][10];  //二维字符数组
char c[20]={'c', '  ', 'p', 'r', 'o', 'g', 'r', 'a','m'};  // 给部分数组元素赋值
char d[]={'c', ' ', 'p', 'r', 'o', 'g', 'r', 'a', 'm' };  // 对全体元素赋值时可以省去长度

在C语言中,字符串总是以\0 作为串的结束符。上面的两个字符串,编译器已经在末尾自动添加了\0

C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。

  • char *string 是指针
  • char string[] 是数组

以下面四个字符串为例:

 char *string1 = "I Love U";
 char *string2 = "I Love U";
 char string3[] = "I Love U";
 char string4[] = "I Love U";

结论是:
string1 == string2 成立
string3 == string4 不成立

因为,string1 和 string2 是指针,为了节省内存,C/C++吧常量字符串放到一个单独的内存区域,所以,当几个指针指向相同的常量字符串时,它们实际上指向的是同一个地址。
string3 和 string4 是数组,我们会为他们分配两个长度为 9 个字节的空间,并把 "I Love U" 的内容分别复制到两个数组中去,所以这是两个起始地址不同的数组。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 27, 2018

C/C++ 中的 const

指针使用 CONST 的几种情况:

(1)指针本身是常量不可变

     char* const pContent; 

(2)指针所指向的内容是常量不可变

     const char* pContent;

(3)两者都不可变

      const char* const pContent; 

参考:

@ShannonChenCHN
Copy link
Owner Author

Include 防范和 pragma once

1. #include防范

在C和C++编程语言中,#include防范,有时被称作宏防范,用于处理#include 指令时,可避免重复引入的问题。在标头档加入#include防范是一种让档案等幂的方法。

例如,


#ifndef Queue_h
#define Queue_h

// 你的代码

#endif /* Queue_h */

参考:

2. pragma once

在C和C++编程语言中,#pragma once是一个非标准但是被广泛支持的前置处理符号,会让所在的文件在一个单独的编译中只被包含一次。以此方式,#pragma once提供类似include防范的目的,但是拥有较少的代码且能避免名称的碰撞。

示例代码:

#pragma once

// 你的代码

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 27, 2018

C++ 中的引用类型

定义:
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

// 声明简单的变量
   int    i;
   double d;
 
   // 声明引用变量
   int&    r = i;
   double& s = d;
   
   i = 5;
   cout << "Value of i : " << i << endl;                         // 5
   cout << "Value of i reference : " << r  << endl;      // 5 
 
   d = 11.7;
   cout << "Value of d : " << d << endl;                      // 11.7
   cout << "Value of d reference : " << s  << endl;    // 11.7

引用通常用于函数参数列表和函数返回值。

void Swap(int& a, int& b) {
    
    int temp = a;
    a = b;
    b = temp;
}

int main(int argc, const char * argv[]) {

    int a = 0;
    int b = 3;

    Swap(a, b);
    
    return 0;
}


C++之所以增加引用类型, 主要是把它作为函数参数,以扩充函数传递数据的功能。

C++ 中函数传参有三种形式:

(1)将变量名作为实参和形参。这时传给形参的是变量的值,传递是单向的。如果在执行函数期间形参的值发生变化,并不传回给实参。因为在调用函数时,形参和实参不是同一个存储单元。// 同 c

(2) 传递变量的指针。形参是指针变量,实参是一个变量的地址,调用函数时,形参(指针变量)指向实参变量单元。这种通过形参指针可以改变实参的值。// 同 c

(3) C++提供了 传递变量的引用。形参是引用变量,和实参是一个变量,调用函数时,形参(引用变量)指向实参变量单元。这种通过形参引用可以改变实参的值。

引用和指针很相似,但也有所不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 28, 2018

C++ 命名规则

类/结构

除了异常类等个别情况(不希望用户把该类看作一个普通的、正常的类之情况)外,C++类/结构 的命名应该遵循以下准则:

  • C++ 类的名称都要以大写字母“C”开头,后跟一个或多个单词。为便于界定,每个单词的首字母要大写(驼峰命名法)。比如:CQueue
  • C++结构体的名称首字母大写,并且也是采用驼峰命名法。例如:BinaryTree
  • 推荐的组成形式:类的命名推荐用"名词"或"形容词+名词"的形式,例如:"CAnalyzer", "CFastVector"

不同于C++结构体的概念,传统的C结构体只是一种将一组数据捆绑在一起的方式。 传统C结构体的名称全部由大写字母组成,单词间使用下划线界定,例如:"SERVICE_STATUS", "DRIVER_INFO" ....

函数

函数的命名 函数的名称由一个或多个单词组成。为便于界定,每个单词的首字母要大写。
推荐的组成形式 函数名应当使用"动词"或者"动词+名词"(动宾词组)的形式。例如:"GetName()", "SetValue()", "Erase()", "Reserve()" ....
保护成员函数 保护成员函数的开头应当加上一个下划线“_”以示区别,例如:"_SetState()" ....
私有成员函数 类似地,私有成员函数的开头应当加上两个下划线“__”,例如:"__DestroyImp()" ....
虚函数 虚函数习惯以“Do”开头,如:"DoRefresh()", "_DoEncryption()" ....
回调和事件处理函数 回调和事件处理函数习惯以单词“On”开头。例如:"_OnTimer()", "OnExit()" ....

变量

变量应该是程序中使用最多的标识符了,变量的命名规范可能是一套C++命名准则中最重要的部分。

变量的命名

变量名由作用域前缀+类型前缀+一个或多个单词组成。为便于界定,每个单词的首字母要大写。对于某些用途简单明了的局部变量,也可以使用简化的方式,如:i, j, k, x, y, z ....

作用域前缀

作用域前缀标明一个变量的可见范围。作用域可以有如下几种:

前缀 说明
局部变量
m_ 类的成员变量(member)
sm_ 类的静态成员变量(static member)
s_ 静态变量(static)
g_ 外部全局变量(global)
sg_ 静态全局变量(static global)
gg_ 进程间共享的共享数据段全局变量(global global)

类型前缀

类型前缀标明一个变量的类型,可以有如下几种:

前缀 说明
n 整型和位域变量(number)
e 枚举型变量(enumeration)
c 字符型变量(char)
b 布尔型变量(bool)
f 浮点型变量(float)
p 指针型变量和迭代子(pointer)
pfn 特别针对指向函数的指针变量和函数对象指针(pointer of function)
g 数组(grid)
i 类的实例(instance)对于经常用到的类,也可以定义一些专门的前缀,如:std::string和std::wstring类的前缀可以定义为"st",std::vector类的前缀可以定义为"v"等等。

推荐的组成形式

变量的名字应当使用"名词"或者"形容词+名词"。例如:"nCode", "m_nState","nMaxWidth" ....

常量

C++中引入了对常量的支持,常量的命名规则如下:

  • 常量名由类型前缀+全大写字母组成,单词间通过下划线来界定,如:cDELIMITER, nMAX_BUFFER ....
  • 类型前缀的定义与变量命名规则中的相同。

枚举、联合、typedef

枚举、联合及typedef语句都是定义新类型的简单手段,它们的命名规则为:
枚举、联合、typedef语句生成的类型名由全大写字母组成,单词间通过下划线来界定,如:FAR_PROC, ERROR_TYPE ....

宏、枚举值

宏和枚举值由全大写字母组成,单词间通过下划线来界定,如:ERROR_UNKNOWN, OP_STOP ....

参考:

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 28, 2018

内联函数

1. 内联函数是什么?

内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。

内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理。

2. 为什么需要内联函数?

内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。

如果没有内联函数,编译器可以决定哪些函数内联 。程序员很少或没有权利控制哪些只能是内联的,哪些不可以内联,作用是程序员可以选择内联的特定应用 。

3. 内联函数与一般函数的区别?

内联函数具有一般函数的特性,它与一般函数所不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。

4. 内联函数和宏定义的区别

宏:

#define TABLE_COMP(x) ((x)>0?(x):0)

内联函数的功能和预处理宏的功能相似。

宏定义和普通函数的比较:
函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。
而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个
函数更有效率。

不过宏也有很多的不尽人意的地方:

  • 宏不能访问对象的私有成员
  • 宏的定义很容易产生二义性

内联函数和宏的区别在于,宏是由预处理器对宏进行替代(预编译时),而内联函数是通过编译器控制来实现的(编译时)。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

通常,在C语言中,内联展开的功能由带参宏(Macros)在源码级实现。内联提供了几个更好的方法:

  • 宏调用并不执行类型检查,甚至连正常参数也不检查,但是函数调用却要检查。
  • C语言的宏使用的是文本替换,可能导致无法预料的后果,因为需要重新计算参数和操作顺序。
  • 在宏中的编译错误很难发现,因为它们引用的是扩展的代码,而不是程序员键入的。
  • 许多结构体使用宏或者使用不同的语法来表达很难理解。内联函数使用与普通函数相同的语言,可以随意的内联和不内联。
  • 内联代码的调试信息通常比扩展的宏代码更有用。

5. 注意事项

(1)内联函数也有一定的局限性,就是函数中的执行代码不能太多了。
如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。

所以,在内联函数内不适合用循环语句和 switch 语句。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。

内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。 

(3)内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。
 
(4)我们可以用Inline来定义内联函数,不过,任何在类内部定义的函数都会被自动的认为是内联函数。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 29, 2018

构造函数和析构函数

class MyClass
{
     public:
       MyClass(void);
       ~MyClass(void);
};

// 构造函数
MyClass::MyClass(void)
{
    //函数体
};

// 析构函数
MyClass::~MyClass(void)
{
    //函数体
};

构造函数

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。

析构函数

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,例如~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

参考

@ShannonChenCHN
Copy link
Owner Author

C++ 动态内存

C++ 程序中的内存分为两个部分:

栈:在函数内部声明的所有变量都将占用栈内存。
堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
很多时候,您无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。

在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。

如果您不需要动态分配内存,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

为普通数据类型动态分配内存

int main ()
{
   double* pvalue  = NULL; // 初始化为 null 的指针
   pvalue  = new double;   // 为变量请求内存
 
   *pvalue = 29494.99;     // 在分配的地址存储值
   cout << "Value of pvalue : " << *pvalue << endl;
 
   delete pvalue;         // 释放内存
 
   return 0;
}

数组的动态内存分配

// 动态分配,数组长度为 m
int *array=new int [m];
 
//释放内存
delete [] array;

对象的动态内存分配

delete 与 delete[] 区别

针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可。
针对类Class,两种方式体现出具体差异:
如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

  • delete ptr -- 代表用来释放内存,且只用来释放ptr指向的内存。
  • delete[] rg -- 用来释放rg指向的内存,!!还逐一调用数组中每个对象的 destructor!!

对于像 int/char/long/int*/struct 等等简单数据类型,由于对象没有 destructor,所以用 delete 和 delete [] 是一样的!但是如果是C++ 对象数组就不同了!

malloc() 函数和 new 的区别

malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Mar 30, 2018

@ShannonChenCHN
Copy link
Owner Author

指针

  • 指针是什么?
  • 指针的作用是什么?

参考

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 1, 2018

静态全局变量 VS. 全局变量

1.全局变量

全局变量在整个程序运行时间都会存在,它被存储在内存中的静态存储区。
全局变量的作用域是整个程序,在定义全局变量的文件内部可以直接使用该变量,但是如果要在定义全局变量的文件外使用该变量,就需要在使用的文件中使用 extern关键字进行变量的引用声明。

例如:
fileA.c

int num = 10; // 定义一个全局变量

void function() {
        printf("%d", num);
}

fileB.c

extern int num; // 通过 extern 进行引用声明(extern 只是声明,不是定义)

void printNum() {
        printf("%d", num);
}

2.静态变量

用静态变量来声明一个变量的作用有两个:

  • 对局部变量来说,它使变量由动态存储方式变成静态存储方式,使得该变量在整个程序执行期间不释放,为其分配的内存空间始终存在。
  • 对全局变量来说,它使全局变量局部化(局部于定义该变量的文件内),但仍为静态存储方式。

下面来具体看看第二种情况:

fileA.c

static int num = 10; // 定义一个静态的全局变量,因此只能在本文件内部使用

void function() {
        printf("%d", num);
}

3. 比较

比较以下两种全局变量定义的区别:

#import "Person.h"

NSString *key = @"xxxx";   // 外部文件可以通过声明 extern 来使用该变量

@implementation Person

@end
#import "Person.h"

static NSString *key = @"xxxx";  // 声明了 static 后,该全局变量只限于本文件使用,而不能被其他文件使用

@implementation Person

@end

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 20, 2020

Integer Overflows

问题:

通过 NSArray-indexOfObject: 方法拿到某个元素的索引后,再将这个索引值加 1,理论上得出的结果应该永远是大于 0 的,结果出现了值为 0 的情况,初步判断是数值越界导致的。

示例代码:

        NSArray *nums = @[@1, @2];
        
        // 9223372036854775807
        // 二进制:0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
        // 十六进制:7fff ffff ffff ffff
        // When building 32-bit applications, NSUInteger is a 32-bit unsigned integer.
        // A 64-bit application treats NSUInteger as a 64-bit unsigned integer
        NSUInteger index = [nums indexOfObject:@0];
        NSInteger notFound = NSNotFound;  // NSNotFound == NSIntegerMax == LONG_MAX
        printf("\n%lx, %lx\n", (unsigned long)index, notFound);
        
        //  9223372036854775807
        // 二进制:0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
        // 十六进制:7fff ffff ffff ffff
        int64_t index64 = index;
        printf("\n%llx\n", index64);
        
        // -1
        // 二进制: 0000 0000 0000 0000 0000 0000 0000 0001 -> 1111 1111 1111 1111 1111 1111 1111 1111
        // 十六进制:ffff ffff
        int32_t index32 = (int32_t)index;
        printf("\n%x\n", index32);
        
        // 7fffffffffffffff
        // 二进制:0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
        // 十六进制:7fff ffff ffff ffff
        // When building 32-bit applications, NSInteger is a 32-bit integer.
        // A 64-bit application treats NSInteger as a 64-bit integer.
        NSInteger indexLong = index;
        printf("\n%lx\n", (long)indexLong);
        
        // 9223372036854775808
        // 二进制:  1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
        // 十六进制:8000 0000 0000 0000
        NSUInteger indexPlusOne64 = index + 1;
        printf("\n%lx\n", (unsigned long)indexPlusOne64);
        
        // 0
        // 二进制:0000 0000 0000 0000 0000 0000 0000 0000
        // 十六进制:0000 0000
        int indexPlusOne32 = (int)(index + 1);
        printf("\n%x\n", indexPlusOne32);

结论:

在编码时,要注意大数问题,避免出现越界/溢出的情况。苹果官方推荐的检查方式是使用 clang 内置的计算检查函数,比如 __builtin_umull_overflow

延伸阅读

@ShannonChenCHN
Copy link
Owner Author

ShannonChenCHN commented Jun 12, 2021

枚举

枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
对于上面的代码,在编译的某个时刻会变成类似下面的样子:

#include <stdio.h>

int main(){
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
    scanf("%d", &day);
    switch(day){
        case 1: puts("Monday"); break;
        case 2: puts("Tuesday"); break;
        case 3: puts("Wednesday"); break;
        case 4: puts("Thursday"); break;
        case 5: puts("Friday"); break;
        case 6: puts("Saturday"); break;
        case 7: puts("Sunday"); break;
        default: puts("Error!");
    }
    return 0;
}

Mon、Tues、Wed 这些名字都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。

下面是一个开发实践中遇到的问题:
IMG_4505

参考:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant