C++笔记(一)---概述与编译,类型分类

一、C与C++区别

C是面向过程的语言,而C++是面向对象的语言,什么是面向对象?
C和C++动态管理内存的方法不一样,C使用malloc/free函数,而C++除此之外还有new/delete关键字;(关于malooc/free与new/delete的不同?);
关于C中的struct和C++的类,C++的类是C所没有的,但是C中的struct可以在C++中正常使用,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用。唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;
C++支持函数重载,而C不支持函数重载,而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,所以C++才会支持不同的参数调用不同的函数;
C++中有引用,而C没有;引用和指针的区别?;
C++全部变量的默认链接属性是外链接,而C是内连接;
C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&解引用的操作的话,是存放在符号表的,不开辟内存);
当然还有局部变量的声明规则不同,多态,C++特有输入输出流之类的,很多,下面就不再列出来了

二、C++11新特性

longlong类型,auto关键字,constexpr关键字,智能指针,placement new,列表初始化, =default生成默认构造函数,类内初始化,nullptr

三、编译

动态编译与静态编译

静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;
动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

动态联编与静态联编

在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;
静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。
动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。

程序开始到打印到屏幕上的全过程

1.用户告诉操作系统执行HelloWorld程序(通过键盘输入等)

2.操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。

3.操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。

4.操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。

5.执行helloworld程序的第一条指令,发生缺页异常

6.操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序

7.helloword程序执行puts函数(系统调用),在显示器上写一字符串

8.操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程

9.操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区

10.视频硬件将像素转换成显示器可接收和一组控制数据信号

11.显示器解释信号,激发液晶屏

12.OK,我们在屏幕上看到了HelloWorld

C语言的编译链接过程

源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件

预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。

编译阶段
编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件

汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件

链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

四、变量

C++定义了一套包括算术类型(arithmetic type)和空类型(void) 在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如当函数不返回任何值时使用空类型作为返回类型。

复合类型

左值与右值

在C++中,一个左值是指向一个指定内存的东西。右值就是不指向任何地方的东西。通常来说,右值是暂时的,而左值以变量的形式(variable)存在。

int x = 666; //ok

在这里,666是一个右值。一个数字(从技术角度来说他是一个字面常量(literal constant))没有指定的内存地址,当然在程序运行时一些临时的寄存器除外。在该例中,666被赋值(assign)给xx是一个变量。一个变量有着具体(specific)的内存位置,所以是个左值。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。
对于左值x,你可以做像这样的操作:

int* y = &x;  //ok

这里通过取地址操作符&获取了x的内存地址并且把它放进了y&操作符需要一个左值并且产生了一个右值,这也是另一个完全合法的操作:在赋值操作符的左边有一个左值(一个变量),在右边使用取地址操作符产生的右值。然而,我们不能这样写:

int y;
666 = y; //error!

从技术上来说是因为666是一个字面常量也就是一个右值,它没有一个具体的内存位置(memory location),所以会把y分配到一个不存在的地方。

C++11中引入了右值引用和移动语义,可以避免无谓的复制,提高了程序的性能,右值引用标记为T&&。所谓右值引用就是必须绑定到右值的引用。通过&&而不是&来获得右值引用。右值引用有一个重要的性质一只能绑定到一个将要销毁的对象。因此可以将一个右值引用的资源“移动”到另一个对象中。
(1)左值和右值是独立于它们的类型,右值引用类型可能是左值也可能是右值
(2) auto&&或函数参数类型的自动推导的T&&是个未定的引用类型,它可能是左值引用,也可能是右值引用,取决于初始化的值类型
(3)所有的右值引用叠加到右值引用上仍然是一个右值引用, 其它引用叠加都为左值引用,当T&&为模版参数时,输入左值,它会变为左值引用,输入右值则变为具名的右值引用
(4)编译器会将已命名的右值引用视为左值,而将未命名的右值视为右值

类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。对于常规引用(为了与右值引用区分开来,称之为左值引用(Ivalue reference)),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

int i  = 42;
int &r = i;            //正确: r引用i
int &&rr = i;          //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i*42;        //错误: 1*42 是一个右值
const int &r3 = i*42;  //正确:可以将一个const的引用绑定到一个右值上
int &&rr2 = i*42;        //正确:将rr2绑定到乘法结果上

int &&rrl = 42;  //正确:字面常量是右值
int &&rr2 = rr1; //错误:表达式rr1是左值!

变量是左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
在对象拷贝的时候要先开辟一块内存然后把原来内存上的数据复制过去,然后释放掉原来的内存。这样效率有点低,用引用来实现对象的移动的想法是用引用声明一个变量作为原来的对象的别名然后销毁原来的对象(或者对象自己销毁自己),然后就可以通过引用来操作原来对象内存上的数据了,实现对象的移动。在C++98时代这种想法的实现是困难的,因为用引用去绑定到一个对象以后,原来那个对象怎么自己销毁自己?但是在C++11引入右值以后就有办法了,就是把原来对象强行转换成右值,我们知道右值是转瞬即逝的,然后再定义一个右值引用绑定到该对象强行转换后的右值上,就大功告成了。在C++11中,可以通过std:move()函数将左值对象转成右值对象方便右值引用对其绑定,从而实现对象移动。

引用

int a=1;int &b=a。1、引用变量b和被引用变量a并没有共用一块内存,b是另外开辟了一块内存

2、引用变量b开辟的内存中存放的是a的地址

3、任何对变量b的操作,都将转换为对(*b)的操作,比如b=b+1实际上是(*b)=(*b)+1 而(*b)代表的就是a
4、基于上面3点可以总结出,引用变量b可以理解为被引用变量a的别名
​再看一个实际的例子

#include<iostream>using namespace std;int main(){
    int  a = 1;
    int&  b = a;
    cout << "a:address->" << &a << endl;
    cout << "b:address->" << &b << endl;

    getchar();
    return 0;        }
运行结果: 
a:address->0031FD54 
b:address->0031FD54

1.引用必须在声明引用时将其初始化,而不能先声明,再赋值。也不能在使用过程中途对其赋值企图更改被引用的值,那样是无效的,比如:
int rats = 101;
int & rodents = rats; 
int bunnies = 50;
rodents = bunnies;  
在上面一通操作以后rodent引用的还是rats
2.在用引用作为函数形参的时候,如果实参与引用参数不匹配,C++将生成临时参数。使用临时参数的数据改变不会影响传入的数据。比如:

long a=3,b=5;
swap(a,b);
void swap(int &a,int &b)
{
    int temp;
 
    temp=a;
    a=b;
    b=temp;
}

这里的a,b与传入函数参数的a,b类型不匹配,因此编译器将创建两个临时int变量,将它们初始为3和5,然后交换临时变量的内容,而a和b保持不变。

右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

指针

指针(pointer) 是“指向(point to)"另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。引用的底层是通过指针实现的。然而指针与引用相比又有很多不同点。

1、指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。引用初始化后不可赋值。

2、指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

3、访问对象方式,一个是直接一个是解引用。

4、赋值操作方式,一个是直接,一个是取地址。

5、sizeof,一个是对象大小一个是自己大小。

6、有多级指针,无多级引用

7、自增运算,对引用的操作直接反应到所指向的对象;而对指针的操作,会使指针指向下一个对象。

函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。

//比较两个string对象的长度
bool lengthCompare(const string &,const string &) ;
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf) (const string &,const string &) ; //未初始化
pf = lengthCompare;        // pf指向名为lengthCompare的函数
pf = &lengthCompare;       //等价的赋值语句:取地址符是可选的

bool b1 = pf("he1lo","goodbye");     // 调用lengthCompare函数
bool b2 = (*pf) ("hello","goodbye"); //一个等价的调用
bool b3 = lengthCompare("he11o","goodbye"); // 另一个等价的调用

 要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照上式可以将lengthCompare的地址赋给pf。此外还能直接使用指向的数的指针调用该函数,无须提前解引用指针。

指针与数组

通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素.因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针。很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。

string nums[] = {"one", "two", "three");. //数组的元素是string对象.
string *p = &nums[0] ;            // p指向nums的第一个元素
string *P2- nums;                 //等价于p2 = &nums[0]

int ia[] = 10,1,2,3.4,5,6.7,8,91; // ia是一个含有10个整数的数组
auto ia2(ia);         // ia2 是一个整型指针,指向ia的第一个元素.
ia2 = 42;             //错误: ia2是一个指针,不能用int值给指针赋值
尽管ia是由10个整数构成的数组,但当使用ia作为初始值时,编译器实际执行的初始化过程类似于下面的形式:
auto ia2(&ia[0]); //显然1a2的类型是int*

在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组作为一个auto 变量的初始值时,推断得到的类型是指针而非数组。

当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由10个整数构成的数组:

//ia3 是一个含有10个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6, 7,8,9};
ia3 = P;    //错误:不能用整型指针给数组赋值
ia3[4] = i; //正确;把i的值賦給ia3的一个元素

指针常量:int * const p,地址不可修改,内容可以

常量指针:const int *p,地址可修改,内容不可修改

野指针:指向内存被释放或没有访问权限的区域。由于1指针未初始化2对象内存释放后指针未置NULL3指针超出了作用范围 而产生。

数组

字面值常量

普通

一个形如42的值被称作字面值常量(literal), 这样的值- -望而知。每个字面值常量都对应一种数据类型, 字面值常量的形式和值决定了它的数据类型。包括整型和浮点型字面值,字符和字符串字面值,转义序列等。

枚举类型

枚举类型(enumeration) 可以将一组整型常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。C++包含两种枚举:限定作用域的和不限定作用域的。C++11 新标准引入了限定作用
域的枚举类型(scoped enumeration)。 

enum class open_modes { input, output, append};// class可改为struct
定义一个名为open_modes的限定作用域的枚举类型,包含三个枚举成员
定义不限定作用域的枚举类型时省略掉关键字class (或struct),枚举类型的名字是可选的
enum color {red, yellow, green) ;    //不限定作用城的枚举类型
//未命名的、不限定作用域的枚举类型
enum {floatPrec = 6,doublePrec = 10, double_doublePrec = 10};

enum color {red, yellow,green};    //不限定作用域的枚举类型
enum stoplight { red, yellow, green};//错误:重复定义了枚举成员
enum class peppers {red, yellow,green}; // 正确:枚举成员被隐藏了
color eyes = green; //正确:不限定作用域的枚举类型的枚举成员住于有效的作用城中
peppers p = green;  // 错误: peppers 的枚举成员不在有效的作用城中
                    // color::green 在有效的作用域中,但是类型错误
color hair = color::red;   //正确:允许显式地访问枚举成员
peppers p2 = peppers::red; //正确:使用pappers的red

 默认情况下,枚举值从0开始,依次加1。不过也能为枚举成员指定专门的值: 

enum class intTypes {
    charTyp = 8,shortTyp = 16, intTyp = 16,
    longTyP = 32,1ong_1ongTyp = 64
};

由枚举成员intTyp和shortTyp可知,枚举值不一定唯一。 如果没有显式地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。
枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式。也就是说每个枚举成员本身就是一条常量表达式,可以在任何需要常量表达式的地方使用枚举成员。

一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型。如果没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int.对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。如果我们指定了枚举成员的潜在类型(包括对限定作用域的enum的隐式指定),则一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误。

自定义类型:结构体

C语言struct和C++struct,class的区别

C语言struct成员默认的是public, C++默认private。C语言struct不是类,不可以有函数,也不能使用类的特征例如public等关键字 ,也不可以有static关键字。C结构体不能继承;C中使用结构体一般结合typedef,C++使用结构体可直接使用。

typedef struct Book{
    int num;
    char name;
};MyBook
MyBook book;
否则得:struct Book book;

struct(public)和class(private)定义的类之间唯一的差别就是默认成员访问权限及默认派生访问权限,除此之外,再无其他不同之处。

内存对齐方式

默认的对齐方式:各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照对齐方式调整位置,空缺的字节自动填充。
为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后还会根据需要自动填充空缺的字节。

(1)示例代码一:
struct MyStruct
{
     double dda1;
     char dda;
     int type;
};//错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
  //对:当在VC中测试上面结构的大小时,会发现sizeof(MyStruct)为16。

(1)先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;
(2)接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,
所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用sizeof(char)=1个字节;
(3)接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,
不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,自动填充3个字节,
这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,
该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,
刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。
所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,其中有3个字节是VC自动填充的。
 
(2)示例代码二:交换上述例子中MyStruct的成员变量的位置
struct MyStruct
{
     char dda;
     double dda1;
     int type;
 };//错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
   //对:当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为24。
 
(1)先为第一个成员dda分配空间,其起始地址跟结构的起始地址相同(偏移量0刚好为sizeof(char)的倍数),
该成员变量占用sizeof(char)=1个字节;
(2)接下来为第二个成员dda1分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为1,
不是sizeof(double)=8的倍数,需要补足7个字节才能使偏移量变为8(满足对齐方式),
因此自动填充7个字节,dda1存放在偏移量为8的地址上,它占用8个字节;
(3)接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为16,
是sizeof(int)=4的倍数,满足int的对齐方式,所以不需要自动填充,type存放在偏移量为16的地址上,
该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:1+7+8+4=20,
不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruct)为1+7+8+4+4=24。其中总的有7+4=11个字节是自动填充的。 

内存对齐原因:平台移植型好,cpu处理效率高
不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能只在某些地址访问某些特定类型的数据,抛出硬件异常,及遇到未对齐的边界直接就不进行读取数据了。

从上图看出对应两种存储方式,若CPU的读取粒度为4字节,那么对于一个int 类型,若是按照内存对齐来存储,处理器只需要访存一次就可以读取完4个字节。若没有按照内存对其来读取,如上图所示,就需要访问内存两次才能读取出一个完整的int 类型变量。
具体过程为,第一次拿出 4个字节,丢弃掉第一个字节,第二次拿出4个字节,丢弃最后的三个字节,然后拼凑出一个完整的 int 类型的数据。结构体内存对齐是拿空间换取时间的做法。提高效率

 大小端模式

大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址端。
小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。

计算机系统中是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(看具体的编译器)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

判断大小端模式:

直接读取存放在内存中的十六进制数值,取低位进行值判断

int a = 0x12345678;

int *c = &a;

c[0] == 0x12   大端模式

c[0] == 0x78   小段模式

也可以用union来进行判断,或查看内存的方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值