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<<buf<<endl;
我们再来看看C++
的输入流cin
和cout
, 可以看到的是, cin
操作char buf[10]
同样不安全, 也会造成写越界.
char buf[10];
cin.getline(buf, 10);
cout<<buf<<endl;
我们可以看到, cin.getline
很好的解决了这个问题, 但其实作用和fgets
并无二异.
string buf;
cin>>buf;
cout<<buf<<endl;
然而使用string
代替char buf[10]
避免char[]
安全问题, 才是C++
的正确打开方式, 这下无论怎样乱搞, 都不会有问题.
int data = 1234;
cout<<hex<<data<<endl;
cout<<oct<<data<<endl;
cout<<dec<<data<<endl;
接下来, 我们来看看, C++
的进制转换, 使用<<hex
,代表16进制, <<oct
, 代表8进制, <<dec
,代表10进制.
4d2
2322
1234
Program ended with exit code: 0
使用<<hex
切换进制, 注意虽然默认是10进制但切换其他进制后, 默认为切换后的进制.
int data = 1234;
cout<<data<<endl;
cout<<setw(10)<<setiosflags(ios::left)<<data<<endl;
cout<<setw(10)<<setiosflags(ios::right)<<data<<endl;
使用setw
和setiosflags
来设置域宽和左右对齐.
#include <iomanip>
在使用setw
和setiosflags
之前需要添加头文件.
1234
1234
1234
Program ended with exit code: 0
以上就是使用setw
和setiosflags
的打印结果.
int a = 12;
int b = 3;
int c = 5;
cout<<setfill('0')<<setw(2)<<a<<":"<<setw(2)<<b<<":"<<setw(2)<<c<<endl;
使用setfill
进行填充, 这个就不多说了, 试试就知道.
float f = 1.23456;
cout<<f<<endl;
cout<<setprecision(4)<<f<<endl;
cout<<setprecision(4)<<setiosflags(ios::fixed)<<f<<endl;
使用setprecision
来调整浮点数的精度.
函数重载
函数重载, 会出现重名的函数, 重名的函数会根据语境来决定调用, 运算符重载也是一种函数重载.
void func(int a) {
cout<<"void func(int a)"<<endl;
}
void func(float a) {
cout<<"void func(float a)"<<endl;
}
void func(char a) {
cout<<"void func(char a)"<<endl;
}
int main(int argc, const char * argv[]) {
int a = 1;
func(a);
float b = 1.2;
func(b);
char c = 'c';
func(c);
return 0;
}
void func(int a)
void func(float a)
void func(char a)
函数重载是一种简洁的需要, 函数返回值类型不能构成函数重载的标志.
重载底层实现使用命名倾轧来改变函数名, 区分不同参数不同的同名函数.
#ifndef mystr_h
#define mystr_h
#include <stdio.h>
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<<"=="<<endl;
} else {
cout<<"!="<<endl;
}
return 0;
}
Pos ps = {1, 2};
Pos fdPs = {3, 4};
if (operator==(ps, fdPs)) {
cout<<"=="<<endl;
} else {
cout<<"!="<<endl;
}
运算符重载其实也就是函数调用.
默认参数
OC
没有默认参数, 而C++
却有...., Swift
也有这个特性.
void foo(int a = 1, int b = 2, int c = 3) {
cout<<"====="<<endl;
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main(int argc, const char * argv[]) {
foo();
foo(2);
foo(2, 3);
foo(2, 3, 4);
return 0;
}
=====
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<<endl;
cout<<"ra = "<<ra<<endl;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
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<<a<<"-"<<b<<endl;
return 0;
}
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<<endl;
cout<<"q = "<<q<<endl;
swap(&p, &q);
cout<<"p = "<<p<<endl;
cout<<"q = "<<q<<endl;
swap(p, q);
cout<<"p = "<<p<<endl;
cout<<"q = "<<q<<endl;
return 0;
}
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<<parr[i]<<endl;
}
int (& rarr)[10] = arr;
cout<<"sizeof = "<<sizeof(rarr)<<endl;
return 0;
上面是数组的引用, 数组的引用呢, 在OC
上就是*
, 而底层原来是这样实现的.
int foo() {
int a = 200;
return a;
}
int main(int argc, const char * argv[]) {
const int & c = 100;
cout<<c<<endl;
int a = 3; int b = 5;
const int & ret = a + b;
cout<<ret<<endl;
const int & ra = foo();
cout<<ra<<endl;
double d = 100.12;
double & rd = d;
const int & rd2 = d;
cout<<rd<<endl;
cout<<rd2<<endl;
rd = 200.14;
cout<<rd<<endl;
cout<<rd2<<endl;
return 0;
}
100
8
200
100.12
100
200.14
100
Program ended with exit code: 0
常引用, 引用的是一个寄存器常量. 和宏在预编译期间替换不同, 常引用在汇编期间通过寄存器替换.
void foo(int & ri, char & rc) {
cout<<sizeof(ri)<<" "<<sizeof(rc)<<endl;
}
struct TypeC {
char c;
};
struct TypeP {
char * pc;
};
struct TypeR {
char & rc;
};
void mySwap(int * pa, int * pb) {
int t = *pa;
*pa = *pb;
*pb = t;
}
void mySwap(int & ra, int & rb) {
int t = ra;
ra = rb;
rb = t;
}
int main(int argc, const char * argv[]) {
int a; char c;
foo(a, c);
cout<<"sizeof(TypeC) = "<<sizeof(TypeC)<<endl;
cout<<"sizeof(TypeP) = "<<sizeof(TypeP)<<endl;
cout<<"sizeof(TypeR) = "<<sizeof(TypeR)<<endl;
int n = 3, m = 5;
mySwap(&n, &m);
cout<<n<<" "<<m<<endl;
mySwap(n, m);
cout<<n<<" "<<m<<endl;
return 0;
}
我们等下用汇编来对比一下指针和引用之前的区别.
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<<endl;
int **pp1 = (int **)malloc(sizeof(int *));
int **pp2 = new int *;
pp1 = pp2;
struct Stu {
float score;
char name[30];
char sex;
};
Stu * ps1 = (Stu *)malloc(sizeof(Stu));
Stu * ps2 = new Stu;
cout<<sizeof(*ps1)<<" "<<sizeof(*ps2)<<endl;
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<<pf1[i]<<" "<<pf2[i]<<endl;
}
char ** pp = new char * [10];
for (int i = 0; i < 10; i++) {
pp[i] = "Castiel";
}
pp[10] = nullptr;
while (*pp) {
cout<<*pp++<<endl;
}
int (* p)[5] = new int[3][5];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
p[i][j] = i + j;
}
}
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
cout<<p[i][j]<<" ";
}
cout<<endl;
}
int (* p2)[3][5] = new int[2][3][5];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 5; k++) {
p2[i][j][k] = i + j + k;
}
}
}
对于连续的空间, 也就是数组来说, 我们可以使用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<<i<<endl;
}
} catch (bad_alloc & e) {
cout<<"内存申请异常 "<<e.what()<<endl;
}
... 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<<"内存申请异常"<<endl;
exit(1);
}
int main(int argc, const char * argv[]) {
double * pd[50];
set_new_handler(newError);
for (int i = 0; i< 50; i++) {
pd[i] = new double[500000000000];
cout<<i<<endl;
}
return 0;
}
...
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__<<endl;
}
}
...
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<double>(i);
i = static_cast<int>(d);
d = static_cast<double>(10) / 3;
cout<<d<<endl;
void * p; int * q;
p = q;
q = static_cast<int *>(p);
3.33333
Program ended with exit code: 0
static_cast
隐式转化
int * m; int n;
m = reinterpret_cast<int *>(n);
reinterpret_cast
指针与数值之间进行转换
void foo(const int & a) {
const_cast<int &>(a) = 200;
}
int main(int argc, const char * argv[]) {
int a;
const int & ra = a;
a = 100;
cout<<a<<endl;
const_cast<int &>(ra) = 300;
cout<<ra<<endl;
cout<<a<<endl;
const int * p = &a;
*const_cast<int *>(p) = 400;
cout<<*p<<endl;
foo(a);
cout<<a<<endl;
return 0;
}
const_cast
只作用与指针和引用, 去const
化
const int a = 100;
const int & ra = a;
const_cast<int &>(ra) = 200;
cout<<a<<endl;
cout<<ra<<endl;
100
200
对于const
修饰的值, 是不能改变的,
命名空间
对于OC
是用前缀, 对于Java
是用包名, 对于Swift
也有和C++
一样的命名空间.
void foo() {
cout<<"foo"<<endl;
}
int mm = 100;
int main(int argc, const char * argv[]) {
std::cout<<::mm<<endl;
::foo();
return 0;
}
::
全局无名命名空间.
namespace ONE {
int x = 4;
}
namespace ANOTHER {
int x = 14;
}
int main(int argc, const char * argv[]) {
{
int x = 250;
cout<<ONE::x<<endl;
cout<<ANOTHER::x<<endl;
cout<<x<<endl;
}
{
using ONE::x;
cout<<x<<endl;
}
{
using namespace ANOTHER;
cout<<x<<endl;
}
return 0;
}
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<<ONE::ANOTHER::x<<endl;
return 0;
}
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<<a<<" "<<b<<endl;
return 0;
}
同名命名空间自动合并.
string
字符串是C++
比 C
高级的地方, 操作起来也比C
简单太多, 也比OC
简单太多, 问OC
为啥那么麻烦.
int * pi = new int(10);
cout<<pi<<endl;
cout<<*pi<<endl;
string * p = new string("Castiel");
cout<<p<<endl;
cout<<*p<<endl;
char * q = "Castiel";
cout<<q<<endl;
cout<<*q<<endl;
0x100506740
10
0x10050c710
Castiel
Castiel
C
Program ended with exit code: 0
虽然string
是一种类, 但已经可以和int的地位相同了.
string s;
cout<<sizeof(string)<<endl;
cout<<sizeof(s)<<endl;
string s1("Castiel");
string s2 = "Castiel";
cout<<s1<<" "<<s2<<endl;
cin>>s;
cout<<s<<endl;
getline(cin, s); //解决了空格的问题
cout<<s<<endl;
string s3 = "Great Wall";
cout<<s3.size()<<endl;
string s4 = " in China";
cout<<(s3 += s4)<<endl;
string s5 = "Great Wall";
if (s3 == s5) {
cout<<"=="<<endl;
} else {
cout<<"!="<<endl;
}
string s6;
cout<<(s6 = s3)<<endl;
string s7 = to_string(1234);
cout<<s7<<endl;
string s8 = "123abc";
cout<<stoi(s8)<<endl;
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<<endl;
}
private:
int _year;
int _month;
int _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<<endl;
}
};
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<<endl;
}
void Date::printDate() {
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
int main(int argc, const char * argv[]) {
Date * date = new Date(1992, 06, 19);
date->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<<a<<endl;
}
其实类名本质就是一个命名空间. 怎么又是命名空间了呢, 再笑....
最后
更多新鲜文章可以关注并Star
, 我们一起学习.
GitHub Repo:coderZsq.github.io
Follow: coderZsq · GitHub