转载

C++ 一把窥探OC底层的利刃

C++ 一把窥探OC底层的利刃

GitHub Repo:coderZsq.github.io 

Follow: coderZsq · GitHub 

Resume: coderzsq.github.io/coderZsq.we…

日常扯淡

作为iOS开发的菜鸡, 平日里的工作就是做业务, 调UI, 对于我们这种弱鸡玩家来说, 编程呢, 其实就是调方法, 调属性, 调库...

但光是做业务UI的工作肯定会让自己日渐乏味, 为了不重复写那些看了想吐的代码, 去年就花了点时间写了一个代码生成工具, 用于配置一键生成垃圾代码的, 这样对于菜鸡开发者也就是我来说, 只需要调调自己封装的一些UI库, 搭搭积木就完成工作了, 其他时间就可以自由支配玩点有趣的事情.

去年浑浑噩噩, 学了一堆有的没的, 什么Vue, React, Spring的调用, 但一直这样无所事事的我, 今年也想深入的学习一些什更深层次的东西, 而不仅仅只是在UI层的浅尝辄止.

然而在iOS这个领域中, 想要深入研究, C/C++, 汇编, Linux, 像三座大山一样拦在我面前, 所以做为菜鸡的我路漫漫而其修远兮.

本文就通过学习C++的语法开始, 一点一点的剖析OC的本质, 直到世界的尽头.

C++ 作者的建议

在C++中几乎不需要用宏, 用const和enum定义显式的常量, 用inline避免函数调用的额外开销, 用模板去刻画一组函数或类型, 用namespace去避免命名冲突.

不要在你需要变量之前去声明, 以保证你能立即对它进行初始化.

不要用malloc, new运算会做的更好.

避免使用void*, 指针算数, 联合和强制, 大多数情况下, 强制类型转换是设计错误的指示器.

尽量少用数组和C风格的字符串, 标准库中的string和vector可以简化程序.

更加重要的是, 试着将程序考虑为一组由类和对象表示的互相作用的概念年, 而不是一堆数据结构和一些可以拨弄的二进制.

标准输入输出

我们先来看C++的标准输入输出:

char buf[10];
scanf("%s", buf);
printf("%s/n", buf);

这个是大家都很熟悉的C的输入输出, 也就是scanf和printf. 但C语言是越界不检的, 所以这里的char buf[10]的缓冲区可能会造成写越界, 导致不安全访问.

char buf[10];
fgets(buf, 10, stdin);
printf("%s/n", buf);

所以C使用了fget强制的截断了输入来保证, 访问的安全.

12345678901234567
123456789
Program ended with exit code: 0

以上是C语言安全标准输入的打印日志, 我们可以看到, 由于设置了10为输入截断参数, 后面就不再输入了.

char buf[10];
cin>>buf;
cout<

我们再来看看C++的输入流cin和cout, 可以看到的是, cin 操作char buf[10] 同样不安全, 也会造成写越界.

char buf[10];
cin.getline(buf, 10);
cout<

我们可以看到, cin.getline很好的解决了这个问题, 但其实作用和fgets并无二异.

string buf;
cin>>buf;
cout<

然而使用string代替char buf[10] 避免char[]安全问题, 才是C++的正确打开方式, 这下无论怎样乱搞, 都不会有问题.

int data = 1234;
cout<

接下来, 我们来看看, C++的进制转换, 使用<

4d2
2322
1234
Program ended with exit code: 0

使用<

int data = 1234;
cout<

使用setw和setiosflags来设置域宽和左右对齐.

#include

在使用setw和setiosflags之前需要添加头文件.

1234
1234      
      1234
Program ended with exit code: 0

以上就是使用setw和setiosflags的打印结果.

int a = 12;
int b = 3;
int c = 5;    
cout<

使用setfill进行填充, 这个就不多说了, 试试就知道.

float f = 1.23456;
cout<

使用setprecision来调整浮点数的精度.

函数重载

函数重载, 会出现重名的函数, 重名的函数会根据语境来决定调用, 运算符重载也是一种函数重载.

void func(int a) {
    cout<<"void func(int a)"<
void func(int a)
void func(float a)
void func(char a)

函数重载是一种简洁的需要, 函数返回值类型不能构成函数重载的标志.

重载底层实现使用命名倾轧来改变函数名, 区分不同参数不同的同名函数.

#ifndef mystr_h
#define mystr_h

#include extern "C" int myStrlen(const char *s);

#endif /* mystr_h */

#include "mystr.h"
//extern "C" {
int myStrlen(const char *s) {
    int len = 0;
    while (*s) {
        len++;
        s++;
    }
    return len;
}
//}

C++默认进行倾轧, 使用extern "C" 来避免倾轧造成的链接错误. 用来连接C的库.

运算符重载

和上面讲的相同, 运算符重载的本质其实也是一种函数重载, 相信熟悉Swift的你, 一定不会陌生.

typedef struct _pos {
    int x_;
    int y_;
} Pos;

bool operator== (Pos one, Pos another) {
    if (one.x_ == another.x_ && one.y_ == another.y_) {
        return true;
    } else {
        return false;
    }
}

int main(int argc, const char * argv[]) {
    
    Pos ps = {1, 2};
    Pos fdPs = {3, 4};
    if (ps == fdPs) {
        cout<<"=="<默认参数

OC没有默认参数, 而C++却有...., Swift也有这个特性.

void foo(int a = 1, int b = 2, int c = 3) {
    cout<<"====="<
=====
a = 1
b = 2
c = 3
=====
a = 2
b = 2
c = 3
=====
a = 2
b = 3
c = 3
=====
a = 2
b = 3
c = 4

默认参数和函数重载可能会产生冲突. 优先选择默认参数的方案.

引用

引用的本质是对指针的包装, 避免使用裸露的指针, 引用是一种声明关系, 不开辟空间, 这里说不开辟, 只是代码层面看, 实际开始会开辟空间的.

int a = 100;
int &ra = a;
cout<<"a = "<
a = 100
ra = 100
&a = 0x7ffeefbff5cc
&ra = 0x7ffeefbff5cc
Program ended with exit code: 0
void swap(int &ra, int &rb) {
    ra ^= rb;
    rb ^= ra;
    ra ^= rb;
}

int main(int argc, const char * argv[]) {
    
    int a = 10;
    int b = 20;
    swap(a, b);
    cout<
20-10

传引用等于传作用域, 这一点熟悉OC的同学其实根本不用在意.

void swap(char **a, char **b) {
    char *t = *a;
    *a = *b;
    *b = t;
}

void swap(char * &ra, char * &rb) {
    char *t = ra;
    ra = rb;
    rb = t;
}

int main(int argc, const char * argv[]) {
    
    char *p = "china";
    char *q = "canada";
    cout<<"p = "<
p = china
q = canada
p = canada
q = china
p = china
q = canada

上面是指针的引用, 并没有引用的指针.

int * p;
int ** pp = &p;
int *** ppp = &pp;
int **** pppp = &ppp;
    
int a;
int &ra = a;
int &rb = ra;
int &rc = rb;

引用为平级, 没有指针的指针这种概念.

int arr[10] = {1, 2, 3 ,4, 5, 6, 7};
int * const & parr = arr;
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
    cout<

上面是数组的引用, 数组的引用呢, 在OC上就是*, 而底层原来是这样实现的.

int foo() {
    int a = 200;
    return a;
}

int main(int argc, const char * argv[]) {
    
    const int & c = 100;
    cout<
100
8
200
100.12
100
200.14
100
Program ended with exit code: 0

常引用, 引用的是一个寄存器常量. 和宏在预编译期间替换不同, 常引用在汇编期间通过寄存器替换.

void foo(int & ri, char & rc) {
    cout<

我们等下用汇编来对比一下指针和引用之前的区别.

    0x100001060 <+0>:  pushq  %rbp
    0x100001061 <+1>:  movq   %rsp, %rbp
    0x100001064 <+4>:  movq   %rdi, -0x8(%rbp)
    0x100001068 <+8>:  movq   %rsi, -0x10(%rbp)
    0x10000106c <+12>: movq   -0x8(%rbp), %rsi
    0x100001070 <+16>: movl   (%rsi), %eax
    0x100001072 <+18>: movl   %eax, -0x14(%rbp)
    0x100001075 <+21>: movq   -0x10(%rbp), %rsi
    0x100001079 <+25>: movl   (%rsi), %eax
    0x10000107b <+27>: movq   -0x8(%rbp), %rsi
    0x10000107f <+31>: movl   %eax, (%rsi)
->  0x100001081 <+33>: movl   -0x14(%rbp), %eax
    0x100001084 <+36>: movq   -0x10(%rbp), %rsi
    0x100001088 <+40>: movl   %eax, (%rsi)
    0x10000108a <+42>: popq   %rbp
    0x10000108b <+43>: retq

指针的汇编

    0x100001090 <+0>:  pushq  %rbp
    0x100001091 <+1>:  movq   %rsp, %rbp
    0x100001094 <+4>:  movq   %rdi, -0x8(%rbp)
    0x100001098 <+8>:  movq   %rsi, -0x10(%rbp)
    0x10000109c <+12>: movq   -0x8(%rbp), %rsi
    0x1000010a0 <+16>: movl   (%rsi), %eax
    0x1000010a2 <+18>: movl   %eax, -0x14(%rbp)
    0x1000010a5 <+21>: movq   -0x10(%rbp), %rsi
    0x1000010a9 <+25>: movl   (%rsi), %eax
    0x1000010ab <+27>: movq   -0x8(%rbp), %rsi
    0x1000010af <+31>: movl   %eax, (%rsi)
->  0x1000010b1 <+33>: movl   -0x14(%rbp), %eax
    0x1000010b4 <+36>: movq   -0x10(%rbp), %rsi
    0x1000010b8 <+40>: movl   %eax, (%rsi)
    0x1000010ba <+42>: popq   %rbp
    0x1000010bb <+43>: retq

引用的汇编

引用的本质是个指针, 必须初始化, 长指针, 一经声明不可改变. 类似于 int * const p.

new 和 delete

new和delete, 还有new[], delete[], 是用来代替malloc和free的, 两者之间不能串用.

int * p1 = (int *)malloc(sizeof(int));
int * p2 = new int;
*p2 = 100;
cout<<*p1<<" "<<*p2<
0 100
36 36

以上是new 和 malloc的比较.

float * pf1 = (float *)malloc(10 * sizeof(float));
float * pf2 = new float[10]{1.2, 3.4};
for (int i = 0; i < 10; i++) {
    cout<

对于连续的空间, 也就是数组来说, 我们可以使用new[], 来开辟堆内存.

0 1.2
0 3.4
0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 0
Castiel
Castiel
Castiel
Castiel
Castiel
Castiel
Castiel
Castiel
Castiel
Castiel
0 1 2 3 4 
1 2 3 4 5 
2 3 4 5 6

上述代码的打印日志.

int * p = new int;
delete p;
int ** pp = new int * [10];
delete []pp;
int (*ppp)[5] = new int[3][5];
delete []ppp;

delete 与 delete[], 就是释放内存和连续的内存.

char ** p = new char * [10];
for (int i = 0; i < 10; i++) {
    p[i] = new char[10];
}
for (int i = 0; i < 10; i++) {
    delete []p[i];
}
delete []p;

释放由内向外, 层级释放.

try {
    double * pd[50];
    for (int i = 0; i< 50; i++) {
        pd[i] = new double[500000000000];
        cout<
... cpp
C++(37659,0x100395340) malloc: *** mach_vm_map(size=4000000000000) failed (error code=3)
*** error: can't allocate region
*** set a breakpoint in malloc_error_break to debug
内存申请异常 std::bad_alloc
Program ended with exit code: 0

对于堆内存申请失败的异常捕获的第一种方式, try-catch, 貌似OC中很少用到, 因为OC可以给空对象发送消息.

void newError( ) {
    cout<<"内存申请异常"<
...
C++(37705,0x100395340) malloc: *** mach_vm_map(size=4000000000000) failed (error code=3)
*** error: can't allocate region
*** set a breakpoint in malloc_error_break to debug
内存申请异常
Program ended with exit code: 1

第二种是使用set_new_handler回调函数来进行捕获.

double * pd[50];
for (int i = 0; i< 50; i++) {
    pd[i] = new (nothrow)double[500000000000];
    if (pd[i] == nullptr) {
        cout<<"内存申请异常 "<<" "<<__FILE__<<" "<<__func__<<" "<<__LINE__<
...
C++(37787,0x100395340) malloc: *** mach_vm_map(size=4000000000000) failed (error code=3)
*** error: can't allocate region
*** set a breakpoint in malloc_error_break to debug
内存申请异常  /Users/zhushuangquan/Desktop/C++/C++/main.cpp main 19

第三种则是不进行异常捕获...., 三种情况优先使用try-catch.

inline 内联函数

inline int sqr(int x) {
    return x * x;
}

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

    int i = 0;
    while (i < 5) {
        printf("%d/n", sqr(i++));
    }
    return 0;
}

代替宏函数, 会在代码段出现多个副本, 但取决于编译器优化, 适用函数体小并被频繁调用,

强制类型转换

尽量不要强转, 强转是设计不足导致的.

double d; int i;
d = static_cast(i);
i = static_cast(d);

d = static_cast(10) / 3;
cout<
3.33333
Program ended with exit code: 0

static_cast 隐式转化

int * m; int n;
m = reinterpret_cast(n);
reinterpret_cast 指针与数值之间进行转换
void foo(const int & a) {
    const_cast(a) = 200;
}

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

    int a;
    const int & ra = a;
    a = 100;
    cout<

const_cast只作用与指针和引用, 去const化

const int a = 100;
const int & ra = a;
    
const_cast(ra) = 200;
cout<
100
200

对于const修饰的值, 是不能改变的,

命名空间

对于OC是用前缀, 对于Java是用包名, 对于Swift也有和C++一样的命名空间.

void foo() {
    cout<<"foo"<

::全局无名命名空间.

namespace ONE {
    int x = 4;
}

namespace ANOTHER {
    int x = 14;
}

int main(int argc, const char * argv[]) {
    
    {
        int x = 250;
        cout<
4
14
250
4
14
Program ended with exit code: 0

命名空间的使用, 命名空间只能定义在全局.

第一种推荐使用, 第二种少用, 第三种禁用.

namespace ONE {
    int x = 4;
    namespace ANOTHER {
        int x = 14;
    }
}

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

    cout<
14
Program ended with exit code: 0

命名空间的嵌套.

namespace ONE {
    int a = 4;

}
namespace ONE {
    int b = 14;
}

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

    using namespace ONE;
    cout<

同名命名空间自动合并.

string

字符串是C++ 比 C高级的地方, 操作起来也比C简单太多, 也比OC简单太多, 问OC为啥那么麻烦.

int * pi = new int(10);
cout<
0x100506740
10
0x10050c710
Castiel
Castiel
C
Program ended with exit code: 0

虽然string是一种类, 但已经可以和int的地位相同了.

string s;
cout<>s;
cout<
24
24
Castiel Castiel
10
Great Wall in China
!=
Great Wall in China
1234
123
Program ended with exit code: 0

上述是string的基本使用, 没啥技术含量.

class

在C++中, 结构体和类的本质没有什么具体的区别, 只是权限访问上有些许不同, 而不是像以前认为的类是引用传递, 而结构体是值传递, 传地址, 不也是值么, 只不过能取地址... 笑.

struct Date {
    
    void init(int year = 1970, int month = 01, int day = 01) {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void printDate() {
        cout<<_year<<"-"<<_month<<"-"<<_day<

struct默认全部是public.

class Date {
    
    int _year;
    int _month;
    int _day;
    
public:
    void init(int year = 1970, int month = 01, int day = 01) {
        _year = year;
        _month = month;
        _day = day;
    }
    
    void printDate() {
        cout<<_year<<"-"<<_month<<"-"<<_day<

class默认全部是private.

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

    Date * date = new Date;
    date->init(1992, 06, 19);
    date->printDate();
    delete date;

    Date * date2 = new Date;
    date2->init(2012, 12, 21);
    date2->printDate();
    delete date2;

    return 0;
}
1992-6-19
2012-12-21
(lldb) p/x date
(Date *) $0 = 0x000000010050e9c0
(lldb) p/x date2
(Date *) $1 = 0x000000010050f2b0
(lldb) 
Program ended with exit code: 0

从打印上来说, 结构体和类是一样的, 可以说类的本质就是结构体指针, 但其实类也可以不用指针引用, 就变成了值传递? 又笑.

class Date {
    
private:
    int _year;
    int _month;
    int _day;
    
public:
    Date(int year = 1970, int month = 01, int day = 01);
    ~Date();
    void printDate();
};

Date::Date(int year, int month, int day)
:_year(year), _month(month), _day(day){}

Date::~Date() {
    cout<<"delete: "<<_year<<"-"<<_month<<"-"<<_day<printDate();
    delete date;
    
    Date * date2 = new Date(2012, 12, 21);
    date2->printDate();
    delete date2;
    
    return 0;
}
1992-6-19
delete: 1992-6-19
2012-12-21
delete: 2012-12-21
Program ended with exit code: 0

class多文件的基本用法, 构造器, 析构器, 参数列表什么的就不多说了.

namespace xxx {
    int a;
    void log();
}

void xxx::log() {
    a = 120;
    cout<

其实类名本质就是一个命名空间. 怎么又是命名空间了呢, 再笑....

最后

更多新鲜文章可以关注并Star, 我们一起学习.

GitHub Repo:coderZsq.github.io 

Follow: coderZsq · GitHub

正文到此结束
Loading...