C++基础知识与语法 C++编译过程 数据类型 变量 指针

C++编译过程

编译器

编译器,就是将程序(C、C++、Java等)翻译成计算器能够读懂的二进制指令。 C++等语言使用编译器编译连接所写程序生成可执行文件,JAVA等语言使用JVM虚拟机将程序翻译成字节码并交给虚拟机执行。

gcc与g++

gcc是GNU计划(打造出一套完全自由(即自由使用、自由更改、自由发布)、开源的操作系统)的产物之一,起初是专门针对C语言的编译器,不过后来经过发展,还可以作为C++、Go、Objective -C 等多种编译语言编写的程序的编译器,现在已经可以称之为“GNU 编译器套件”。

gcc和g++

gcc可以编译C和C++(实际上gcc、g++指令是对ccp(预处理指令)、cc1(编译指令)、as(汇编指令)指令的包装),两者的主要区别在于:

  • gcc会根据文件名后缀去自行判断出文件类型,如果遇到文件xx.c则默认以编译 C 语言程序的方式编译此文件,遇到文件xx.cpp,则默认以编译 C++ 程序的方式编译此文件。
  • g++命令无论目标文件的后缀名是什么,都一律按照编译 C++ 代码的方式编译该文件。因为C++兼容C语言,所以就算遇到xx.c,g++同样也可以以C++方式编译

但gcc编译C++代码会比g++更加繁琐一些,默认无法找到标准库以及类对象等从而导致报错,因此推荐使用g++编译器。

使用g++编译器通过参数指定编译过程,如-o参数直接指定生成可执行文件:g++ main.cpp -o test

编译过程

编译器具体的编译过程可以分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)

image_CYztZ1gzp_.png

  1. 预处理:真正的编译前的准备工作,主要处理源文件和头文件中以#开头的命令,如 #include、#define、#ifdef 等。预处理的结果是生成.i文件。.i文件也是包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。
  2. 编译:把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后生成汇编.s文件。
  3. 汇编:将汇编代码转化为机器码的过程,主要是汇编语句和机器指令的对照表一一翻译。生成后缀为o的目标文件。
  4. 链接:引入代码中使用到的“库”文件,生成可执行文件。

数据类型

C++是静态语言,声明变量必须指定类型,类型是C++编程的基础。类型规定了对象的存储要求和所能执行的操作。 C++提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。

1
2
3
4
5
6
7
8
9
c++复制代码short s = 100; //短整型,16位
int i = 100; // 整型,16位或32位,编译器决定
long l = 100; // 长整型,32位
long long ll = 100; //c++11引入,64位
float f = 200.0; // 单精度浮点数,6位有效数字
double d = 200.0; // 双精度浮点数,10位有效数字
char c = 'd'; // 8位
//字符串
char *str = "JakePrim";

内置类型的机器实现:为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容

带符号类型和无符号类型通过在类型名前添加unsigned就可以得到无符号类型,如unsigned long,类型unsigned int可以缩写为unsigned

自动类型转换

当在程序的某处使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换

1
2
3
4
5
6
c++复制代码bool b = 12;          // true
int i = b; // i的值为1
i = 3.14; // i的值为3
double pi = i; // pi的值为3.0
unsigned char c = -1; // 假设 char占8比特,c的值为255
signed char c2 = 256; // 假设char占8比特,c2的值是未定义的

编译器自动进行类型转换,因此一种常用的技巧是使用算数值直接作为条件判断:while(i){...}

自动类型转换容易导致出错,且需要注意:当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数,因此切勿混用带符号类型和无符号类型

1
2
3
4
c++复制代码unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
std::cout << u + i << std::endl; // 如果int占3位,输出4294967264

浮点型字面值可以表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:

1
2
c++复制代码3.14159 
3.14159e0

变量

变量定义的基本形式是:类型说明符后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:

1
c++复制代码int sum = 0, value;

当对象在创建时获得了一个特定的值,就说这个对象被初始化(initialized)了。用于初始化变量的值可以是任意复杂的表达式。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。 因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量:

1
c++复制代码double price = 109.99, discount = price * 0.16;

在C++中,初始化是一个异常复杂的问题,初始化和赋值是两个完全不同的操作。初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,以一个新值来替代。

默认初始化

如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。如,string类规定如果没有指定初值则生成一个空串:

1
c++复制代码std::string empty; // empty非显式地初始化为一个空串

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定(作用域)。定义于任何函数体之外的变量被初始化为0。但是,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误,因此最好显示初始化变量。

1
2
3
4
5
c++复制代码int main(){
int c;
cout << c << endl; // 不是默认0,函数体内部的内置类型变量将不被初始化
return 0;
}

作用域

C++中大多数作用域以花括号分隔。C++中大多数作用域以花括号分隔。文件内为全局作用域,函数有函数作用域。
需要注意,不应该返回局部变量的地址,因为局部变量会在函数返回后被销毁。返回的地址指向一个过期的对象,后面可能发生不可预知的行为。

指针

每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址指针指向某个内存地址,通过*访问内存地址的值(访问指针的值)。

1
2
3
4
5
6
7
c++复制代码// 取址符 &
int var1;
char var2[10];
cout << "var1 变量的地址: " << &var1 << endl; // var1 变量的地址: 0x61ff0c
cout << "var2 变量的地址: " << &var2 << endl; //var2 变量的地址: 0x61ff02
// 指针声明
int *ip = &var1; // 0x61ff0c

指针是变量内存位置的直接地址,指针存储内存地址,即声明的指针只能赋值为地址,指针是一种类型,与某种数据类型绑定,单独使用指针则是内存地址,使用*访问则是变量值,也可以为原内存中的值赋值。

1
2
3
4
c++复制代码int *ip = var1; //error
cout << ip << endl; //invalid conversion from 'int' to 'int*' [-fpermissive]
int *ip = &val1;
*ip = 0;

空指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值,在C中为NULL,C++中为nullptr尽量使用**nullptr**,访问空指针会使程序崩溃。

1
2
c++复制代码int *ptr = NULL; //空指针
cout << ptr << endl;//0

指针运算

指针是一个用数值表示的地址。因此可以对指针执行算术运算,四种算术运算:++、–、+、-。无法对空指针进行运算。指针会根据数据类型移动指针到下一个内存位置。

1
2
3
4
5
6
7
8
c++复制代码//指针运算
int var[3] = {10, 100, 200};
int *ptr = var;
for (int i = 0; i < 3; i++) {
cout << "Address of var[" << i << "] = " << ptr << endl; // 0x61fee4 0x61fee8 0x61feec
cout << "Value of var[" << i << "] = " << *ptr << endl; // 10 100 200
ptr++; // 移动到下一个位置
}

void*指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,但并不知道该地址中到底是什么类型的对象,常用作函数的输入或输出。一种使用场景:如果期望接口(函数)能够接受任何类型的参数,可以使用void*类型。但是在具体使用的时候,必须转换为具体的指针类型,也就是说必须清楚原始传入的是什么类型,然后转换成对应类型。例如准备对结构体进行排序时需要实现排序接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c++复制代码void qsort(void *base,size_t nmemb,size_t size , int(*compar)(const void *,const void *));
typedef struct student_tag
{
char name[STU_NAME_LEN]; //学生姓名
unsigned int id; //学生学号
int score; //学生成绩
}student_t;
int studentCompare(const void *stu1,const void *stu2)
{
  /*强转成需要比较的数据结构*/
student_t *value1 = (student_t*)stu1;
student_t *value2 = (student_t*)stu2;
return value1->score-value2->score;
}

多级指针

指向指针的指针,单纯的字面意思,和指针的含义一样,地址→存放指针的地址→值

1
2
3
c++复制代码int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = &pi; // ppi指向一个int型的指针

image_ZKjJmLA5-K.png

引用

引用就是变量别名,一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量,引用的最初目的就是为了简化指针的使用,与Java中的引用类似。

1
2
3
c++复制代码int ival = 1024;
int &refVal = ival;
int &refVal2;

引用与指针的区别

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

变量名称是变量附属在内存位置中的标签,可以把引用当成是变量附属在内存位置中的第二个标签因此,可以通过原始变量名称或引用来访问变量的内容,引用必须被初始化,引用可以当做和变量一样使用。

引用作为函数参数传递可以简化指针操作,直接修改实参,引用必须初始化绑定一个变量,不能是常量,但是可以使用常量引用初始化(const)

1
2
c++复制代码int &ref = 10       // 错误
const int &ref = 10 // 可以

const

const主要有以下作用:

  1. 修饰变量,说明该变量不可以被改变
  2. 修饰指针,分为指向常量的指针(指针常量pointer to const)和自身是常量的指针(常量指针,const pointer),const修饰后面的类型
  3. 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改
  4. 修饰成员函数,说明该成员函数内不能修改成员变量
1
c++复制代码const int bufSize = 512; // 输入缓冲区大小 把bufSize定义成了一个常量,任何试图为bufSize赋值的行为都将引发错误

当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换

const对象一旦创建后其值就不能再改变,所以const对象必须初始化,默认状态下,const对象仅在文件内有效

const 与引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

1
2
3
4
c++复制代码const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用,不能修改常量
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象,需要用const修饰

const与指针

指针常量:通常意义上的,指向常量的指针,**const int *p** 。

常量指针:指针是常量,**int *const p**。

1
2
3
4
5
c++复制代码char greeting[] = "Hello";
char *p1 = greeting; // 指针变量,指向字符数组变量
const char *p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char *const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char *const p4 = greeting; // 自身是常量的指针,指向字符数组常量

数组

声明数组: **type arrayName [ arraySize ];** ,C++中必须指定类型与大小,同时大小必须为常量表达式。

1
c++复制代码double balance[10];

可以使用大括号直接初始化数组:

1
2
c++复制代码double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0}; // 数组大小为初始化时元素的个数

数组不允许拷贝和赋值:

1
2
3
c++复制代码int a[] = {1,2,3};
int a2[] = a; // error 不允许拷贝
a2 = a; // error 不允许赋值

在很多情况下数组名称就是指针(数组首地址),可以直接当成指针使用:

1
2
c++复制代码int a[] = {1,2,3};
int *p = a;

声明多维数组,数组访问使用下标:

1
2
3
4
5
6
c++复制代码int threedim[5][10][4];
int a[3][4] = {
{0, 1, 2, 3} ,
{4, 5, 6, 7} ,
{8, 9, 10, 11}
};

自定义数据结构struct

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法,没有初始值的成员将被默认初始化,对象使用**.访问成员,对象指针使用*ptr.ptr→**

1
2
3
4
5
6
7
8
9
10
c++复制代码struct Sales_data {
std::string bookNo = "1";
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sales_data accum, trans;
Sales_data *salesptr = &trans;
std::cout << accum.bookNo << salesptr->bookNo << (*salesptr).bookNo << std::endl;
}

运算符

  • 算术运算符: + - * / % ++ -— ,加减乘除,取余
  • 逻辑运算符: && || !,与或非
  • 关系运算符:> < >= <= == !=

成员访问运算符:对象类型直接使用点运算符,对象类型指针使用 或者(*ptr).

++i 与 i++

1
2
c++复制代码++i // 先i+1,后赋值i
i++ //先赋值i,后i+1

三元表达式

也叫条件运算符:? : ,可以嵌套使用

1
2
c++复制代码cond ? expr1:expr2
int i = (1 < 2) ? 3 : 4

位运算符

  • 左移:左移乘2,<<
  • 右移:右移除2,>>
  • 与:&
  • 异或:^
  • 或:|
  • 求反:
    1
    2
    3
    4
    5
    6
    7


    sizeof运算符
    ---------


    获取类型或表达式结果类型的大小,`sizeof`运算符返回一条表达式或一个类型名所占的字节数。返回一个`size_t`(`size_t`是一种机器相关的无符号类型,它被设计的足够大,以便能表示内存中任意对象的大小)的类型。`sizeof`有两种形式:

c++复制代码sizeof (type)
sizeof expr

1
2
3
4
5
6
7

* sizeof 对数组,得到整个数组所占空间大小
* sizeof 对引用,得到引用对象所占空间大小
* sizeof 对指针,得到指针本身所占空间大小


**因为执行`sizeof`运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数,`sizeof`的返回值是一个常量表达式,所以可以用其结果声明数组的维度。**

c++复制代码constexpr size_t sz= sizeof(ia)/sizeof(*ia)
int arr2[sz];

1
2
3
4
5
6
7

语句
==


if
--

c++复制代码if(boolean_expression 1){
}else if( boolean_expression 2){
}else {
}

1
2
3
4
5
6

switch
------


**`switch`****遇到第一个****`break`****或到末尾才会结束,所以注意不要漏写****`break`,switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。**

c++复制代码switch(expression){
case constant-expression :
statement(s);
break;
// 可以有任意数量的 case 语句
default :
statement(s);
}
char grade = ‘D’;
switch(grade){
case ‘A’ :
cout << “A” << endl;
break;
case ‘B’ :
case ‘C’ :
cout << “C” << endl;
break;
default :
cout << “default” << endl;
}

1
2
3

while
-----

c++复制代码while(condition){
statement(s);
}

1
2
3

for
---

c++复制代码for ( init; condition; increment ){
statement(s);
}

1
2

也常配合`auto`使用执行`foreach`循环:

c++复制代码int my_array[5] = {1, 2, 3, 4, 5};
for (auto &x : my_array) {
x *= 2;
cout << x << endl;
}

1
2
3
4
5
6

do while
--------


**与**`while`**不同的是表达式至少被执行一次后才进行循环判断。**

c++复制代码do{
statement(s);
}while( condition );


break & continue
----------------


`break`跳出循环,`continue`立即从循环判断处重新开始。



**本文转载自:** [掘金](https://juejin.cn/post/7034832575579455519)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*
0%