《iOS面试之C/C++部分》
iOS面试之C/C++部分
https://edu.51cto.com/course/2306.html
虽说这门课是针对iOS面试,但知识点都是C/C++上的。里面有些内容讲的挺好,尤其是前几节对指针的讲解、最后一节关于函数参数以及二级指针的内容。个人认为这两块比较深入透彻,非常值得一看,其他地方相对比较一般,是比较基础、简单的内容。
# 1. 指针
上来几个问题:
1.你是如何理解指针的?
2.谈谈你对指针的理解?
3.指针你用的多吗?
4.那些场景必须使用指针才能实现?
a,指针的本质问题
指针本质上和其他变量一样,也是一种变量。不同的是它存储着变量的地址,而不是一般的数值。类似门牌号,用数字存储即可,因此指针是int类型的变量(和int一样,32位系统中,占4字节;64位系统中,占8字节)。
1 | int a= 3; |
第一处的写法是正确的。
这种本质上是正确的,但是因为int类型变量可以保存很多值, 而且可以参与运算等等,如果直接按照第一行处存储,程序员和编译器都容易弄混,可阅读性很差,分不清q是地址还是数值,这就需要添加一个*号进行标记。
第二处的写法也正确。
因为指针变量p也是一种变量,所以也是可以将其地址存储到另一个指针变量qq中。但是同样因为容易混淆,可阅读性很差。所以再加一个*号,用于标记。
b. 指针的变量类型作用
面试题:定义一个结构体,所占字节为20个,请逐字节输出内存里面的值。
可以随意定义如下一个结构体:
1 | struct test{ |
那么如何将结构体中内容逐字节输出呢?
首先看几个例子
1 | int a[] = {2,3}; //数组a是8个字节 |
同样经过+1之后,pa向后移动并输出了4个字节的内容, 而pb向后移动并输出了1个字节的内容。这是因为+1是指后移了sizeof(对应类型)个字节,不一定是+1个字节。对int*来讲,每次+4个字节,对char*来讲每次+1个字节。
因此,指针的类型决定了访问内存时,是以几个字节为一个单位来访问内存的。
要想逐字节输出结构体内容,可以这么写:
1 | struct test strTest; |
2. const
const变量什么时候初始化?
const变量只能在定义的时候初始化,因为它初始化之后不能再进行赋值。如果定义一个const变量不进行初始化,那么它里面的值是一个随机值,不能使用。
1 | int a; |
a. const和指针:
指向常量的指针——a pointer to const——
1
2
3
4int a = 3;
//不能通过指针去修改指向空间的值
const int * p = &a;常指针——const pointer——
1 | int a = 3; |
双const指针
1
2
3int a = 3;
//不能修改指针的指向(指针不可以修改指向的变量)
const int * const ppa = &a;用非pointer to const 可不可以指向一个非const变量
1
2int b = 3;
const int *pb = &b; //可以用非pointer to const 可不可以指向一个const变量
1
2const int cc = 3;
int *pcc = &c; //不可以这种情况下可以通过pcc修改pcc指针修改指向的const变量,因此编译器可能不会通过
b. const对象
C++和OC中,类中的权限分三种:
- private:私有的,只能在类内使用,子类和类外不可以使用
- protected:保护的,类内和子类可以使用,类外不可以使用
- public:公有的,类内和类外、子类都可以使用
但是friend声明的友类函数,也可以在类外使用private私有成员。
两种方式创建对象
1 | //1.一般创建对象的方式 |
当const和对象结合
1 | const Person *p = new Person(); //const对象指针 |
一般的对象默认调用非const成员函数,但如果该对象被const修饰,那么它将调用const成员函数。
const对象/const对象指针只能调用const成员函数,例如:
const Word word; //默认会调用下面的const成员函数
void print()const { … };
Word word; //默认会调用下面的非常量成员函数
void print() { ... };
3.多态
a. 虚函数
派生类Worker、Student均对基类Person(有一个run的成员函数)进行继承,同时用父类指针指向子类对象:
Person q = new Worker();
q->run(); //调用Person的run函数,
默认情况下,上面的代码将会调用基类的函数,即使这种做法的意图是调用派生类函数。这种情况叫做“对象的行为不符合对象的真实身份”。
但如果基类定义的run函数是虚函数、且派生类均对run进行了重载,下面的代码将会调用子类的对应方法:
Person p = new Student();
Person q = new Worker();
p->run(); //调用Student的run函数
q->run(); //调用Worker的run函数
这样,统一通过基类的指针,就可以访问不同的派生类的虚函数。
但是在OC里面,所有函数都是虚函数,可以直接覆盖,实现多态。并且OC中默认继承的数据成员受保护,不能被直接访问。
b. 基类和派生类构造顺序
一般情况下,父类指针指向父类对象、子类指针指向子类对象时:
构造顺序:先生成父类对象、调用父类的构造函数,再生成子类对象、调用子类的构造函数。(编译器隐式在子类的构造函数处添加了父类构造的参数列表)
析构顺序:先调用子类析构,再调用父类析构。
但如果是父类指针指向子类对象,调用顺序为:父类构造、子类构造、父类析构,不会对子类进行析构。
为了解决这种问题,需要将父类的析构函数声明为虚函数,这样子类析构也是虚函数,就会先调用父类析构函数再调用子类析构函数。
4. 函数参数与二级指针
a. 函数参数
函数只能有一个返回值,但有的时候需要根据函数的运算得到多个变量。可以通过函数的参数实现,将输入的变量和输出的变量统一作为参数传入(但输出的变量应该传入指针或引用),最后还可以返回一个返回值。也就是说,参数可以分为输入参数和输出参数,其中输出参数必须传递指针或者引用。
如果采用参数的方式、并且只用指针,当需要通过函数对一个指针的内容进行改变时,输出参数必须是该指针的指针,也就是二级指针。
b. 二级指针
下面代码的目的是:通过一个函数申请对象的内存空间,并返回将空间地址传给指针参数,希望能在函数外部完成对实参的赋值。
1 | void fun(Person *p){ |
最终运行完fun(p)之后,p仍然是空指针。这是因为函数传递的形参永远是实参的一份拷贝,fun函数中的参数p确实指向了存有Person对象的一块内存空间,但这个p和外部的实参p是两个指针变量,它们在函数运行刚开始时仅仅是内容相同(两个指向同一个地址的指针),后来改变了形参p指向的空间,却不对另一个指针造成影响。
为了实现相应的功能,必须要使用二级指针才可以。重写代码如下:
1 | void fun(Person **p){ |
这样的方式的含义是:将申请到的内存空间的地址,保存到二级指针p指向的内存空间。也就是说,这个二级指针指向的空间保存的内容(二级指针指向的地址里面存放的东西)是申请的Person的空间。这个二级指针参数中保存的内容是外部实参指针p的地址,通过修改二级指针指向地址的值(修改*p,p是形参),也就修改了实参指针p保存的值(等同于修改了实参指针p指向的地址)。如果看不懂,那么把指针当成一个普通的变量,二级指针则是指向这个变量的普通指针,然后思考。
也就是说:函数参数传递永远是值拷贝,要想修改某个变量(该变量可能是指针)的值,只能传递该变量的指针(或引用)。
这样会生成一个指向该变量的指针,通过改变该指针指向的内存空间里的内容的方式改变该变量。如果想修改的变量是普通类型,那么传递一级指针;如果想修改一级指针的值,那么传递二级指针;想修改二级指针的值,就传递三级指针……等等。当然也可以使用引用的方式(虽然系统内部还是用了指针)。