《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
2
3
4
5
6
7
int a= 3;

int *p = &a;
int q = &a; //这一行是否正确(第一处)
int **pp = &p;
int *qq = &p; //这一行呢?(第二处)
int *** ppp = &pp;

第一处的写法是正确的。
这种本质上是正确的,但是因为int类型变量可以保存很多值, 而且可以参与运算等等,如果直接按照第一行处存储,程序员和编译器都容易弄混,可阅读性很差,分不清q是地址还是数值,这就需要添加一个*号进行标记。
第二处的写法也正确。
因为指针变量p也是一种变量,所以也是可以将其地址存储到另一个指针变量qq中。但是同样因为容易混淆,可阅读性很差。所以再加一个*号,用于标记。

b. 指针的变量类型作用

面试题:定义一个结构体,所占字节为20个,请逐字节输出内存里面的值。

可以随意定义如下一个结构体:

1
2
3
4
5
6
7
struct test{
int a;
int b;
int c;
int d;
int e;
}

那么如何将结构体中内容逐字节输出呢?
首先看几个例子

1
2
3
4
5
6
7
int a[] = {2,3};  //数组a是8个字节
int *pa = a;
cout<< *(pa+1) <<endl; //输出3

char b[]= {'c','d'}; //数组b是2个字节
char *pb = b;
cout<< *(pb+1) <<endl; //输出d

同样经过+1之后,pa向后移动并输出了4个字节的内容, 而pb向后移动并输出了1个字节的内容。这是因为+1是指后移了sizeof(对应类型)个字节,不一定是+1个字节。对int*来讲,每次+4个字节,对char*来讲每次+1个字节。
因此,指针的类型决定了访问内存时,是以几个字节为一个单位来访问内存的。

要想逐字节输出结构体内容,可以这么写:

1
2
3
4
struct test strTest;
char *p = &strTest; //定义一个char*数组指向结构体对象
for(int i= 0;i<20;i++) //逐字节输出
cout<< *(p+1) <<endl;

2. const

const变量什么时候初始化?
const变量只能在定义的时候初始化,因为它初始化之后不能再进行赋值。如果定义一个const变量不进行初始化,那么它里面的值是一个随机值,不能使用。

1
2
3
4
5
6
int a;
cout << a <<endl; //输出一个随机值

//下面是等价的两种写法
const int b = 3;
int const c = 4;

a. const和指针:

  1. 指向常量的指针——a pointer to const——

    1
    2
    3
    4
    int a = 3;

    //不能通过指针去修改指向空间的值
    const int * p = &a;
  2. 常指针——const pointer——

1
2
3
int a = 3;
//不能修改指针的指向(指针不可以修改指向的变量)
int * const pa = &a;
  1. 双const指针

    1
    2
    3
    int a = 3;
    //不能修改指针的指向(指针不可以修改指向的变量)
    const int * const ppa = &a;
  2. 用非pointer to const 可不可以指向一个非const变量

    1
    2
    int b = 3;
    const int *pb = &b; //可以
  3. 用非pointer to const 可不可以指向一个const变量

    1
    2
    const int cc = 3;
    int *pcc = &c; //不可以

    这种情况下可以通过pcc修改pcc指针修改指向的const变量,因此编译器可能不会通过

b. const对象

C++和OC中,类中的权限分三种:

  • private:私有的,只能在类内使用,子类和类外不可以使用
  • protected:保护的,类内和子类可以使用,类外不可以使用
  • public:公有的,类内和类外、子类都可以使用
    但是friend声明的友类函数,也可以在类外使用private私有成员。

两种方式创建对象

1
2
3
4
5
6
7
//1.一般创建对象的方式
Person person;

//2.OC中,使用指针创建对象的方式
//这种方式一定要记得用完对象删除指针
Person *p = new person();
delete p;

当const和对象结合

1
2
3
const Person *p = new Person();  //const对象指针
const Person person; //const对象
//p->print(); 错误,即使成员函数不修改数据成员(实例变量)的值,也不能调用

一般的对象默认调用非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
2
3
4
5
6
void fun(Person *p){
p = new Person();
}

Person *p = NULL;
fun(p);

最终运行完fun(p)之后,p仍然是空指针。这是因为函数传递的形参永远是实参的一份拷贝,fun函数中的参数p确实指向了存有Person对象的一块内存空间,但这个p和外部的实参p是两个指针变量,它们在函数运行刚开始时仅仅是内容相同(两个指向同一个地址的指针),后来改变了形参p指向的空间,却不对另一个指针造成影响。
为了实现相应的功能,必须要使用二级指针才可以。重写代码如下:

1
2
3
4
5
void fun(Person **p){
*p = new Person();
}
Person *p = NULL;
fun(p);

这样的方式的含义是:将申请到的内存空间的地址,保存到二级指针p指向的内存空间。也就是说,这个二级指针指向的空间保存的内容(二级指针指向的地址里面存放的东西)是申请的Person的空间。这个二级指针参数中保存的内容是外部实参指针p的地址,通过修改二级指针指向地址的值(修改*p,p是形参),也就修改了实参指针p保存的值(等同于修改了实参指针p指向的地址)。如果看不懂,那么把指针当成一个普通的变量,二级指针则是指向这个变量的普通指针,然后思考。

也就是说:函数参数传递永远是值拷贝,要想修改某个变量(该变量可能是指针)的值,只能传递该变量的指针(或引用)
这样会生成一个指向该变量的指针,通过改变该指针指向的内存空间里的内容的方式改变该变量。如果想修改的变量是普通类型,那么传递一级指针;如果想修改一级指针的值,那么传递二级指针;想修改二级指针的值,就传递三级指针……等等。当然也可以使用引用的方式(虽然系统内部还是用了指针)。