C/C++求职宝典重点笔记整理


1. char c = '\72'; 中的\72代表一个字符,72是八进制数,代表ASCII码字符“:”。

2. 10*a++ 中a先进行乘法运算再自增(笔试中经常喜欢出这类运算符优先级容易混淆的输出问题)。

3. const和static的作用

太常见的问题了,下面给出一个较详细的参考答案:

static关键字:
1)函数体内static变量的作用范围为函数体。不同于auto变量。该变量的内存只被分配一次。因此其值在下次调用时仍维持上次的值。
2)在模块内的static全局变量可以被模块内的所有函数访问。但不能被模块外的其他函数访问。
3)在模块内的static函数只可被这一模块内的其它函数调用。这个函数的使用范围被限制在声明它的模块内。
4)在类中的static成员变量属于整个类所有,对类的所有对象只有一份复制。
5)在类中的static成员函数属于整个类所有,这个函数不接受this指针,因而只能访问类的static成员变量。

const关键字:

1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化。因为以后就没有机会再改变它了。
2)对指针来说,可以指定指针的本身为const,也可以指定指针所指向的数为const。或二者同时为const。
3)在一个函数的声明中,const可以修饰形参,表明它是一个输入参数。在函数内不能改变其值。
4)对于类的成员函数,若指定其为const类型。则表明其是一个常量函数。不能修改类的成员变量。
5)对于类的成员函数,有时候必须指定其返回值为const类型。以使得其返回值不为“左值”。
 
4. 注意sizeof不是函数而是运算符,所以在计算变量所占用空间大小时,括号是可以省略的,但在计算类型大小时括号则不能省略,比如int i = 0; 则sizeof int是错误的。
 
5. 有1,2,…,n的无序数组,求排序算法,并且要求时间复杂度为O(n),空间复杂度O(1),使用交换,而且一次只能交换两个数。

#include <stdio.h>
int main() {
    int a[] = {10, 6, 9, 5, 2, 8, 4, 7, 1, 3};
    int i, tmp;
    int len = sizeof(a) / sizeof(a[0]);
    for(i = 0; i < len;) {
        tmp = a[a[i] - 1];
        a[a[i] - 1] = a[i];
        a[i] = tmp;
        if(a[i] == i + 1) i++;
    }
    for(i = 0; i < len; ++i)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}
 
6. 易误解:如果int a[5], 那么a与&a是等价的,因为两者地址相同。
解答:一定要注意a与&a是不一样的,虽然两者地址相同,但意义不一样,&a是整个数组对象的首地址,而a是数组首地址,也就是a[0]的地址,a的类型是int[5],a[0]的类型是int,因此&a+1相当于a的地址值加上sizeof(int) * 5,也就是a[5],下一个对象的地址,已经越界了,而a+1相当于a的地址加上sizeof(int),即a[1]的地址。
 
7. 如何将一个小数分解成整数部分和小数部分?
要记得利用头文件中的库函数modf,下面是函数原型(记住一些实用的库函数,避免自己重写):
double modf(double num, double *i); // 将num分解为整数部分*i和小数部分(返回值决定)
 

8. 可作为函数重载判断依据的有:参数个数、参数类型、const修饰符;
   不可以作为重载判断依据的有:返回类型。
 
9. 程序输出题:
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = &(a + 1)[3];
printf("%d\n", *p);
输出:5

说明:因为a+1指向a的第二个元素,[3]表示再向后移动3个元素。
 
10. 程序输出题:

 char str1[] = "abc";
 char str2[] = "abc";
 const char str3[] = "abc";
 const char str4[] = "abc";
 const char *str5 = "abc";
 const char *str6 = "abc";
 char *str7 = "abc";
 char *str8 = "abc";
 cout << (str1 == str2) << endl;
 cout << (str3 == str4) << endl;
 cout << (str5 == str6) << endl;
 cout << (str7 == str8) << endl;

输出:0 0 1 1

说明:输出str1~str8的地址为:
0x23aa80
0x23aa70
0x23aa60
0x23aa50
0x23aa48
0x23aa40
0x23aa38
0x23aa30
输出str1~str8内容“abc”的存储地址为:
0x23aa80
0x23aa70
0x23aa60
0x23aa50
0x100403030
0x100403030
0x100403030
0x100403030
可以发现str1~str4中的内容是存在栈上,地址各不相同,而str5~str8的内容都是存储在常量区,所以地址都相同。
 
注意:
char *str = "abc";
printf("%p\n", str1);
cout << &str1 << endl;
上面打印的是字符串 “abc”的地址,下面打印的是 str1 变量的地址。

11. C的结构体和C++结构体的区别
(1)C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。所以C的结构体是没有构造函数、析构函数、和this指针的。
(2)C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
(3)C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。
 
以上都是表面的区别,实际区别就是面向过程和面向对象编程思路的区别:
C的结构体只是把数据变量给包裹起来了,并不涉及算法。
而C++是把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。
C语言中是没有类的概念的,但是C语言可以通过结构体内创建函数指针实现面向对象思想。
 
12. 如何在类中定义常量成员并为其初始化?
解答:只能在初始化列表里对const成员初始化,像下面这样:
class CBook {
public:
    const double m_price;
    CBook() :m_price(8.8) { }
};
下面的做法是错误的:


class CBook {
public:
    const double m_price;
    CBook() {
        m_price = 8.8;
    }
};

而下面的做法虽未报错,但有个warning,也不推荐:


class CBook {
public:
    const double m_price = 8.8; // 注意这里若没有const则编译出错
    CBook() { }
};
 
13. 在定义类的成员函数时使用mutable关键字的作用是什么?
解答:当需要在const方法中修改对象的数据成员时,可以在数据成员前使用mutable关键字,防止出现编译出错。例子如下:

class CBook {
public:
    mutable double m_price; // 如果不加就会出错
    CBook(double price) :m_price(price) { }
    double getPrice() const; // 定义const方法
};
double CBook::getPrice() const {
    m_price = 9.8;
    return m_price;
}

14. 构造函数、拷贝构造函数、析构函数的调用点和顺序问题,如下面这个例子输出是什么?

class CBook {
public:
    CBook() {
        cout << "constructor is called.\n";
    }
    ~CBook() {
        cout << "destructor is called.\n";
    }
};
 
void invoke(CBook book) { // 对象作为函数参数,如果这里加了个&就不是了,因为加了&后是引用方式传递,形参和实参指向同一块地
                          // 址,就不需要创建临时对象,也就不需要调用拷贝构造函数了
    cout << "invoke is called.\n";
}
 
int main() {
    CBook c;
    invoke(c);
}

解答:注意拷贝构造函数在对象作为函数参数传递时被调用,注意是对象实例而不是对象引用。因此该题输出如下:

constructor is called.
invoke is called.
destructor is called. // 在invoke函数调用结束时还要释放拷贝构造函数创建的临时对象,因此这里还调用了个析构函数
destructor is called.

引申:拷贝构造函数在哪些情况下被调用?
(1)函数的参数为类对象且参数采用值传递方式;
(2)将类对象做为函数的返回值。
 
15. C++中的explicit关键字有何作用?
解答:禁止将构造函数作为转换函数,即禁止构造函数自动进行隐式类型转换。
例如CBook中只有一个参数m_price,在构建对象时可以使用CBook c = 9.8这样的隐式转换,使用explicit防止这种转换发生。
 
16. 在C++中,如果确定了某一个构造函数的创建过程,在该构造函数中如果调用了其它重载的构造函数,它将不会执行其它构造函数的初始化列表部分代码,而是执行函数体代码,此时已经退化成普通函数了。例子说明如下:

class CBook {
public:
    double m_price;
    CBook() {
        CBook(8.8);
    }
    CBook(double price) : m_price(price) { }
};
int main() {
    CBook c;
    cout << c.m_price << endl; // 此时并不会输出理想中的8.8
}

17. 静态数据成员只能在全局区域进行初始化,而不能在类体中进行(构造函数中初始化也不行),且静态数据成员不涉及对象,因此不受类访问限定符的限制。
例子说明如下:
class CBook {
public:
    static double m_price;
};
double CBook::m_price = 8.8; // 只能在这初始化,不能在CBook的构造函数或直接初始化

18. C++中可以重载的运算符:new/delete、new[]/delete[]、++等。
    不可以重载的运算符:、.、::、?:、sizeof、typeid、.、**、不能改变运算符的优先级。
 
引申:重载++和–时是怎么区分前缀++和后缀++的?
例如当编译器看到++a(先自增)时,它就调用operator++(a);
但当编译器看到a++时,它就调用operator++(a, int)。即编译器通过调用不同的函数区别这两种形式。
 
19. C++的多态性分为静态多态和动态多态。
静态多态性:编译期间确定具体执行哪一项操作,主要是通过函数重载和运算符重载来实现的;
动态多态性:运行时确定具体执行哪一项操作,主要是通过虚函数来实现的。
 
20. 虚函数原理考点,例如下面程序的输出是什么?

class A {
public:
    virtual void funa();
    virtual void funb();
    void func();
    static void fund();
    static int si;
private:
    int i;
    char c;
};

问:sizeof(A) = ?

解答:
关于类占用的内存空间,有以下几点需要注意:
(1)如果类中含有虚函数,则编译器需要为类构建虚函数表,类中需要存储一个指针指向这个虚函数表的首地址,注意不管有几个虚函数,都只建立一张表,所有的虚函数地址都存在这张表里,类中只需要一个指针指向虚函数表首地址即可。
(2)类中的静态成员是被类所有实例所共享的,它不计入sizeof计算的空间
(3)类中的普通函数或静态普通函数都存储在栈中,不计入sizeof计算的空间
(4)类成员采用字节对齐的方式分配空间
答案:12(32位系统)或16(64位系统)
 
21. 虚继承的作用是什么?
在多继承中,子类可能同时拥有多个父类,如果这些父类还有相同的父类(祖先类),那么在子类中就会有多份祖先类。例如,类B和类C都继承与类A,如果类D派生于B和C,那么类D中就会有两份A。为了防止在多继承中子类存在重复的父类情况,可以在父类继承时使用虚函数,即在类B和类C继承类A时使用virtual关键字,例如:
class B : virtual public A
class C : virtual public A

注:因为多继承会带来很多复杂问题,因此要慎用。


22.处理printf()的参数

问:下面代码会输出什么?

#include<stdio.h> 
int main(void) 

    int a = 10, b = 20, c = 30; 
    printf("\n %d..%d..%d \n", a+b+c, (b = b*2), (c = c*2)); 

    return 0; 
}
答:输出结果是:

110..40..60
这是因为C语言里函数的参数默认是从右往左处理的,输出时是从左往右。


23.会改变自己名字的进程


问:你能写出一个在运行时改变自己进程名的程序吗?


答:参见下面这段代码:


#include<stdio.h> 


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

    int i = 0; 
    char buff[100]; 


    memset(buff,0,sizeof(buff)); 


    strncpy(buff, argv[0], sizeof(buff)); 
    memset(argv[0],0,strlen(buff)); 


    strncpy(argv[0], "NewName", 7); 


    // Simulate a wait. Check the process 
    // name at this point. 
    for(;i<0xffffffff;i++); 


    return 0; 
}


24.void*和C结构体

问:你能设计一个能接受任何类型的参数并返回interger(整数)结果的函数吗?

答:如下:

int func(void *ptr)
如果这个函数的参数超过一个,那么这个函数应该由一个结构体来调用,这个结构体可以由需要传递参数来填充。


25.gets()函数


问:请找出下面代码里的问题:


#include<stdio.h> 
int main(void) 

    char buff[10]; 
    memset(buff,0,sizeof(buff)); 


    gets(buff); 


    printf("\n The buffer entered is [%s]\n",buff); 


    return 0; 
}
答:上面代码里的问题在于函数gets()的使用,这个函数从stdin接收一个字符串而不检查它所复制的缓存的容积,这可能会导致缓存溢出。这里推荐使用标准函数fgets()代替。


26.strcpy()函数


问:下面是一个简单的密码保护功能,你能在不知道密码的情况下将其破解吗?


#include<stdio.h> 


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

    int flag = 0; 
    char passwd[10]; 


    memset(passwd,0,sizeof(passwd)); 


    strcpy(passwd, argv[1]); 


    if(0 == strcmp("LinuxGeek", passwd)) 
    { 
        flag = 1; 
    } 


    if(flag) 
    { 
        printf("\n Password cracked \n"); 
    } 
    else 
    { 
        printf("\n Incorrect passwd \n"); 


    } 
    return 0; 
}
答:破解上述加密的关键在于利用攻破strcpy()函数的漏洞。所以用户在向“passwd”缓存输入随机密码的时候并没有提前检查“passwd”的容量是否足够。所以,如果用户输入一个足够造成缓存溢出并且重写“flag”变量默认值所存在位置的内存的长“密码”,即使这个密码无法通过验证,flag验证位也变成了非零,也就可以获得被保护的数据了。例如:


$ ./psswd aaaaaaaaaaaaa 


Password cracked
虽然上面的密码并不正确,但我们仍然可以通过缓存溢出绕开密码安全保护。


要避免这样的问题,建议使用 strncpy()函数。


作者注:最近的编译器会在内部检测栈溢出的可能,所以这样往栈里存储变量很难出现栈溢出。在我的gcc里默认就是这样,所以我不得不使用编译命令‘-fno-stack-protector’来实现上述方案。


27.main()的返回类型


问:下面的代码能 编译通过吗?如果能,它有什么潜在的问题吗?


#include<stdio.h> 


void main(void) 

    char *ptr = (char*)malloc(10); 


    if(NULL == ptr) 
    { 
        printf("\n Malloc failed \n"); 
        return; 
    } 
    else 
    { 
        // Do some processing 
        free(ptr); 
    } 


    return; 
}
答:因为main()方法的返回类型,这段代码的错误在大多数编译器里会被当作警告。main()的返回类型应该是“int”而不是“void”。因为“int”返回类型会让程序返回状态值。这点非常重要,特别当程序是作为依赖于程序成功运行的脚本的一部分运行时。


28.内存泄露


问:下面的代码会导致内存泄漏吗?


#include<stdio.h> 


void main(void) 

    char *ptr = (char*)malloc(10); 


    if(NULL == ptr) 
    { 
        printf("\n Malloc failed \n"); 
        return; 
    } 
    else 
    { 
        // Do some processing 
    } 


    return; 
}
答:尽管上面的代码并没有释放分配给“ptr”的内存,但并不会在程序退出后导致内存泄漏。在程序结束后,所有这个程序分配的内存都会自动被处理掉。但如果上面的代码处于一个“while循环”中,那将会导致严重的内存泄漏问题!


提示:如果你想知道更多关于内存泄漏的知识和内存泄漏检测工具,可以来看看我们在Valgrind上的文章。


29.free()函数


问:下面的程序会在用户输入’freeze’的时候出问题,而’zebra’则不会,为什么?


#include<stdio.h> 


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

    char *ptr = (char*)malloc(10); 


    if(NULL == ptr) 
    { 
        printf("\n Malloc failed \n"); 
        return -1; 
    } 
    else if(argc == 1) 
    { 
        printf("\n Usage  \n"); 
    } 
    else 
    { 
        memset(ptr, 0, 10); 


        strncpy(ptr, argv[1], 9); 


        while(*ptr != 'z') 
        { 
            if(*ptr == '') 
                break; 
            else 
                ptr++; 
        } 


        if(*ptr == 'z') 
        { 
            printf("\n String contains 'z'\n"); 
            // Do some more processing 
        } 


       free(ptr); 
    } 


    return 0; 
}
答:这里的问题在于,代码会(通过增加“ptr”)修改while循环里“ptr”存储的地址。当输入“zebra”时,while循环会在执行前被终止,因此传给free()的变量就是传给malloc()的地址。但在“freeze”时,“ptr”存储的地址会在while循环里被修改,因此导致传给free()的地址出错,也就导致了seg-fault或者崩溃。

30.使用_exit退出


问:在下面的代码中,atexit()并没有被调用,为什么?


#include<stdio.h> 


void func(void) 

    printf("\n Cleanup function called \n"); 
    return; 



int main(void) 

    int i = 0; 


    atexit(func); 


    for(;i<0xffffff;i++); 


    _exit(0); 
}
这是因为_exit()函数的使用,该函数并没有调用atexit()等函数清理。如果使用atexit()就应当使用exit()或者“return”与之相配合。

猜你喜欢

转载自blog.csdn.net/qinglongzhan/article/details/78608932
今日推荐