你好C++之小tips

记录一些关于平时遇到的C++小点,不方便单独成文,所以集中记录。

tip1、 常量成员函数

常量成员函数是C++类中的一类函数,其具体形式如下示例:

string isbn() const {return bookNo;}

其特点就是参数列表后面的const,这个const所修饰的内容是this指针,指定this指针为指向常量的指针,因此这样产生的效果就是isbn函数必须为只读函数,所以其函数体内不能包含修改对象成员的语句,如:

string isbn() const {bookNo = "abc";}    //error 

这样做的好处在于:
const成员函数可以访问非const对象的非const数据成员 和 const数据成员;也可以访问const对象的内的所有数据成员;
而非const成员函数只能访问非const对象的数据成员,无法访问const对象的数据成员;
因此,在声明一个函数之前,如果确定这个函数不会对数据成员进行修改,则应该将其声明为const成员函数。

tip2、面向对象的三个特性

1、封装:封装体现在将数据与方法模块化,用于隐藏方法的实现细节,具有保护性。
2、继承:继承主要体现在方法的重用
3、多态:多态主要体现在接口的重用上,子类继承父类的方法,可以通过多态性改变所继承方法的特性(通过重写虚函数实现),总结来说就是“一个接口,多种状态”。

其中关于多态的具体解释:多态性使得程序能在运行过程中才决定具体调用的函数(方法),允许父类的指针或引用指向子类对象,由于子类重写了虚函数,父类的指针或引用通过所指向对象的不同而动态的决定所需要调用的函数(方法)。因此,这样的函数地址是在运行期绑定的。

tip3、 编译器会为空类准备哪些东西

1、一字节的占位符
2、一个缺省的构造函数
3、一个拷贝构造函数 Foo(const Foo& other)
4、一个拷贝赋值运算符Foo& Foo::operator=(const Foo& other)
5、一个析构函数~Foo()
6、两个取地址运算符&,一个返回const this,一个返回this);

tip4、 gcc的从c语言转化为程序所经历的过程

1、预处理:预处理器(cpp)根据以字符开头# 开够的命令,修改原始C程序。如hello.c中第一行#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h,并将其直接插入到程序文本中。最后得到hello.i;
2、编译:编译器(ccl)将预处理得到的hello.i,翻译为汇编语言。得到hello.s;
3、汇编:汇编器(as)将hello.s转化为计算机指令,保存在hello.o中,这是一个二进制文件
4、链接:链接器(ld)将hello.o中的标准库函数(如printf.o)链接合并进去,得到可执行程序hello。

gcc处理程序的完整过程示意图

tip5、 指针与引用的区别

1、指针是一个实际的实体,引用只是一个实体的别名,例如引用不能放在vector中;
2、指针可以为空,引用不能为空(引用被创建的时候就要初始化为对象);
3、指针中途可以改变指向位置,引用从一而终;
4、引用没有const(因为引用不是实体),指针有const;
5、sizeof(&)与sizeof(*),一个得到的是对象大小,一个得到的是地址大小;
6、引用与指针的++操作含义不同。

tip6、 C++11的新特性

1、新的关键字auto,自动类型推导,必须初始化;
2、nulllptr,消除了NULL的二义性;
3、范围for语句;
4、lambda表达式;
5、智能指针;
6、move语义,右值引用;
7、std::function:对任意可调用目标进行存储、复制和调用,简而言之就是函数的容器;
8、对并行计算提供了支持,有了std::thread;

这方面的内容,之后会写一个详细的文章进行介绍。

tip7、sizeof与strlen

1、sizeof是一个一元运算符,而strlen为一个函数;
2、sizeof输出的是一个空间的占位大小,而strlen输出的是字符串长度(其内部应该是循环遍历,求长度,并以null或'\0'为结束条件);
3、sizeof可以作用于各种数据结构(也包括类,函数),而strlen只能作用于char*型变量。

#include<iostream>
#include<string.h>

using namespace std;

int main()
{
    char str[10] = "abc";
    cout << "sizeof:" << sizeof(str) << endl;   //输出10 = 1*10
    cout << "strlen:" << strlen(str) << endl;   //输出3

    int Int[10] = {0,1,2};
    cout << "sizeof:" << sizeof(Int) << endl;   //输出40 = 4*10
    //cout << "strlen:" << strlen(Int) << endl;     //error,因为strlen只能作用于char*

    return 0;
}
输出结果

tip8、 构造函数初探

1、默认构造函数:如果编译器发现类中没有声明任何构造函数,则会产生一个合成的默认构造函数。因此如果我们定义了一个其他的构造函数,这必须自己定义一个默认构造函数(=default),因为如果没有定义自己的默认构造函数,类中就没有默认构造函数了。
2、拷贝构造函数:将other的数据拷贝到要构造的对象中 Foo(const Foo& other);如果没有自己声明拷贝构造函数,则编译器会声明一个默认拷贝构造函数(关于默认拷贝构造函数见tip9);
3、移动构造函数(C++11) :将other中的数据“窃取”到要构造的对象中,并需要令other的操作仍然有意义,但是不保证得到结果。 Foo(const Foo&& other)
4、委派构造函数(C++11) : 比如定义一个一般构造函数,而其他构造函数都是通过调用这个一般构造函数完成,则使用那个一般构造函数的构造函数为委派构造函数。
5、还有就是其他的自己定义的构造函数。

关于构造函数的内容,以后会单独写一篇文章。

tip9、什么情况下要写拷贝构造函数

对于凡是需要动态分配的成员或者包含指针成员的类(因为执行拷贝操作的时候需要重新分配一个内存空间,而默认拷贝构造函数是以浅拷贝完成的),需要提供自己的非空析构函数和自己的拷贝构造函数。

扩展:总结来说,下列三种对象需要调用拷贝构造函数:
1、一个对象作为函数参数,以值传递的方式传入函数体;
2、一个对象作为函数返回值,以值传递的方式从函数返回;
3、一个对象用于给另一个对象进行初始化(即拷贝初始化);

tip10、静态链接库与动态链接库

1、当程序与静态库链接时,库中所有的目标文件都会被copy到可执行文件中,造成代码量增多;而动态库是在运行时才将代码copy到内存中。
2、因为静态库是在编译的时候就已经完成copy,所以运行速度快,而动态库是在运行的时候才拷贝需要的函数,所以运行速度慢(可以理解为静态库是以空间换了时间)。
3、动态库是被多个程序共享,而静态库是每个程序拥有自己的一份copy。
4、如果需要对程序中的库进行修改和优化,使用动态库只需要重新将库编译即可;而使用静态库则需要将依赖静态库的文件都重新编译。
5、在编译过程中调用静态库的时候要注意顺序依赖关系,一般将静态库放到最后。

tip11、抽象类

1、抽象类是有纯虚函数的类(=0),不能被实例化,不能被实例化的原因是:假如它能被实例化,那么调用它的纯虚方法没有可以实际执行的代码
举个例子,张三是个人,人是动物,这里张三是实例化的对象,人是类,动物是抽象类,如果去实例化一个动物,我们无法知道该让这个动物长什么样子。
2、抽象类的作用是负责定义接口,后续类可以重写这个接口,不能创建抽象类的对象,可以定义抽象类的派生类对象。

tip12、构造函数可以是虚函数吗?可以是const吗?

1、构造函数不能是虚函数,这是由构造函数逐层调用的特性决定的,当构造派生类对象的时候,会首先调用基类构造函数,然后再调用派生类构造函数;
假如将构造函数设置为虚函数,根据多态特性,在构造派生类的时候会直接调用派生类的构造函数,这样会造成构造不完全的情况。所以构造函数不能进入虚表中。
2、构造函数也不能是const的(const成员函数):当我们在创建一个类的const对象时,直到构造函数完成初始化过程,才能得到其“常量的属性”。因此构造函数在const对象的构造过程中需要向其写值,而const成员函数具有只读属性,不能完成写入操作。

tip13、 析构函数一般为虚函数的原因

1、上面说明了构造函数的逐层调用顺序,而析构函数的调用是相反的过程。
2、当我们对一个指向派生类的基类指针进行析构的时候,如果析构函数不是虚函数,则无法完成对派生类所有成员对象的析构(只能完成继承自基类的数据成员的析构),所以要将析构函数定义为虚函数,由此根据多态的原理完成析构。
3、另外说明,虚函数并不是只有优点没有缺点,其带来的是虚函数表内存开销的代价,所以,当基类不需要向下继承的时候,没必要将析构函数设定为虚函数。

tip14、友元特性

友元函数的作用在于其能访问类的private成员,但它不是类的成员函数。

类的封装性决定了类的private成员只能由成员函数访问,而在某些情况下,需要从外部访问private成员,虽然可以通过成员函数逐级调用,但是还是非常繁琐,由此,引入了友元函数。

友元类 是将一个类定义为了类的友元。友元类中的所有成员函数都是类的友元函数。

15、inline函数的作用

1、优点:指定为内联函数避免频繁调用消耗大量的栈空间。将函数指定为inline,就是在其调用位置展开,从而能对提高运行效率(不用再去寻找函数的实现);
2、缺点:在调用位置展开从而也决定了内联函数的缺点,增大了代码量,从而消耗了内存空间。

16、 struct与class的区别与联系

1、struct默认数据访问控制是public的,而class默认是private的;
2、class可以作为关键字用于定义模板参数(与typename一样);
3、struct更像是一个数据结构的实现体,而class是一个对象的实现体
4、另外,struct能被继承,能实现多态,能包含构造函数。

17、能否用memset实例化一个类

一般不能使用memset实例化一个类,这会造成很多不安全的因素,如果类中包含指针成员(包括虚函数,因为虚函数包含虚函数指针),如果采用memset,则指针的指向可能被清空
所以应该使用构造函数完成对类的初始化。

18、 new与malloc的区别

1、new是C++的运算符,而malloc是库函数;
2、new能在分配内存的时候初始化,而malloc只能分配内存;
3、new能自动计算sizeof,而malloc需要手动计算;如p = new intp = malloc(sizeof(int))
4、new分配出错会抛出异常,而malloc会返回空指针;
5、new分配成功之后直接返回对象指针,而malloc返回的是void的指针,需要强制转化:p = (int*) malloc(sizeof(int)*128)

扩展* delete与free如何知道应该释放多少内存?
new与malloc在分配内存的时候会在内存中添加一个头部,记录分配内存大小,根据头部释放响应内存。

19、extern关键字的作用

告诉编译器,这是一个全局变量或函数,如果在本文件中没有找到相应的变量或函数,可以在后面或其他文件中寻找。

20、能够在const成员函数中调用非const成员函数

这样做是不安全的
因为const成员函数是只读的,而所调用的非const成员函数可能涉及了成员数据的修改,这样存在一个矛盾,所以说是不安全的。

21、模板的声明与实现是否一定要在同一个头文件中

需要,因为实例化模板的时候,编译器要求必须在上下文中可以查到其定义的实体。

22、const与#define

这两种都能定义一个常量,但是区别在于const是有数据类型的,因此编译器会进行安全性检测;而#define是直接的替换,无法保证数据类型安全。
同理内联函数与#define的宏相比也在于内联函数会做参数类型检查

23、main函数之前都完成了什么

1、将用户参数压入栈区,设置栈指针,完成初始化栈
2、初始化static和global变量;
3、如果存在全局对象则全局对象的构造函数在main之前进行,全局对象的析构函数在main之后完成。

#include<iostream>
using namespace std;

class Pre_Main
{
public:
    Pre_Main()
    {
        cout << "I'm before main!" << endl;
    }
    ~Pre_Main()
    {
        cout << "I'm after main!" << endl;
    }
};

//全局对象
Pre_Main pre_Main;

void main()
{
    cout << "I'm in main!" << endl;
}  
测试结果

24、 extern "C" 的作用

C++头文件中常见到extern "C"修饰的函数,它的作用是用于C++链接在C语言模块中定义的函数

C++虽然兼容C,但在C++文件中函数编译后生成的符号与C语言生成的不同。因为C++支持函数重载,C++函数编译后生成的符号带有函数参数类型的信息,而C则没有。

例如int add(int a, int b)函数经过C++编译器生成.o文件后,add会变成形如add_int_int之类的,而C会变成形如_add,这就导致一个问题,如果C++中使用了C语言实现的函数,在编译链接的时候,就会出错,提示找不到对应符号。此时extern "C"就起作用了:告诉链接器去寻找_add这类的C语言符号,而不是找C++修饰的符号。

实例

//ctest.h
int add(int x, int y);

//ctest.c
#include "ctest.h"
int add(int x, int y) {
    return x+y;
}

//cpptest.cpp
extern "C" {#include "ctest.h"}
int main() {
    add(2,3);
    return 0;
}

tip25、哪些情况下必须使用初始化列表

1、初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。使用初始化列表主要是基于性能问题,对于内置类型,如int,使用初始化列表和在构造函数体内初始化差别不大,但对于类类型而言,最好使用初始化列表。

2、常量成员,引用类型成员必须采用初始化列表形式,因为这两种成员只能初始化,不能赋值

3、对于没有构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来完成初始化,而直接调用拷贝构造函数初始化,这样是非常高效的。
这部分内容以后会单独写一篇文章。

tip26、 static与const能同时修饰变量吗,能同时修饰成员函数吗?

1、static与const是不能同时修饰成员函数的,因为const成员函数的作用是只读函数,不允许对类对象成员变量进行修改,包含一个隐含的this指针;
而static修饰的成员函数,会将this指针缺省,因为它不是归某一个成员变量所有,所以这是矛盾的。

2、static 与 const是可以同时修饰一个变量的。

欢迎转载,转载请注明出处:wenmingxing 你好呀C++

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,540评论 1 51
  • 1.面向对象的程序设计思想是什么? 答:把数据结构和对数据结构进行操作的方法封装形成一个个的对象。 2.什么是类?...
    少帅yangjie阅读 5,051评论 0 14
  • 1.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”? 答:首先,extern是C/C...
    曾令伟阅读 926评论 0 4
  • 1 文件结构 每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件...
    Mr希灵阅读 2,893评论 0 13
  • 李开阳在发完说说后,就把李晓楠移到了皇后分组当中,然后将她的备注改成了:楠小猪。 翌日夜晚,李开阳把李晓楠约到了西...
    死小鱼阅读 112评论 0 0