质朴无华的编程笔记–C++篇

C与C++

  1. C++继承了C的所有特性
  2. C++对类型加强,对类型的检查严格

2-0 C++中struct关键字加强了。 C语言中struct定义了一组变量的集合,而不是一种新的类型。

typedef struct _tag_student Student;//将这个集合定义成类型
struct _tag_student{ //这是个集合,不是类型
	char* name;
	int age;
};

C++中struct 用于定义一个全新的类型

struct Student{
	char* name;
	int age;
};

2-1 C++对类型的检查严格 C++中所有的标识符必须显式声明类型。C中的默认类型(函数在定义时没有给出类型,默认int)在C++中不合法。


C和C++中int func()与int func(void)的区别?

C: int func()表示返回值为int, 接受任意参数的函数。 int func(void)表示返回值为int的无参函数。

CPP: 它们具有相同的意义,都表示返回值为int的无参函数。 —-

  1. C++支持面向对象

  2. C++所有的变量可以在需要使用时定义,C的变量在作用域开始的位置定义

int c = 0;
for(int i = 1; i <= 3; i++)
{
	for(int j = 1; j <= 3; j++)
	{
		c += i * j;// i, j的作用域仅限for循环
	}
}

对于一些老版本的C语言编译器(某个不知名的老版本gcc),这么写会报错。 但是对于现在的C语言编译器,如gcc 7.5.0, dev-c++,这么写也可以。

  1. 对于register关键字:请求编译器将局部变量存储于寄存器中 C++依旧支持register, 但编译器优化方式不同,C++语言可以取得register变量的地址,C不可以。 早期C语言编译器不会对代码进行优化,register是很好的补充。 早期C++编译器发现程序需要取register变量的地址时,register对变量的声明变得无效。

例如

register int a = 0;
printf("%p\n", &a); //此处取register变量a的地址
//地址是对memory而不是对register的,所以此处register无效

当然,对于现代的C/C++编译器,编译器自己可以进行优化,所以register很少被使用到了。

  1. C语言中,重复定义多个同名的全局变量是合法的。 C++中拒绝这种二义性的做法,不允许定义多个同名的全局变量。

const关键字


C语言中的const

  1. const修饰的变量是只读的,本质上还是变量,会分配存储空间
  2. const修饰的局部变量在栈上分配空间(如果这时候通过指针,还是可以修改它的值), 修饰的全局变量在只读存储区分配空间(如果修改它的值,程序很可能崩溃)
  3. const只在编译期有用,在运行期无用

在C语言如何定义真正意义上的常量呢? 只有枚举 —-

对于C++:

在编译过程中当碰见const声明的时候,向符号表中放入常量。编译过程中,若发现使用常量,则直接以符号表中的值进行替换。

为了兼容C, 在以下两种情况下给对应的常量分配存储空间

  1. 对const常量使用了extern(是全局变量,且需要在其他文件中使用)
  2. 对const常量使用了&操作符 虽然C++编译器可能为const常量分配空间,但不会使用其存储空间中的值。

C++中的const常量类似于宏定义 它们的不同在于: const常量是由编译器处理,编译器对const常量进行类型检查和作用域检查。宏定义由预处理器处理,是单纯的文本替换。

举个例子:

const int A = 1;
const int B = 2;
int arr[A+B] = {0};

在C中这是有问题的,A,B是变量。这里数组的大小是两个变量相加得到的,要道运行的时候才会知道,所以编译器会报错。 在C++中编译则可以通过,因为编译到arr[A + B]时,A,B直接从符号表中取值,所以编译器知道arr[]的大小,是3。

再说说宏

void f1(){
	#define a 1
}

void f2(){
	printf("%d", a);
}

这是可以编译通过的(g++),虽然a是f1()中的宏。因为宏是被预处理器处理的,直接进行文本替换,在编译器编译到printf("%d", a)时,实际上是printf("%d", 1)

这里不难发现(g++),宏是没有作用域概念的。而const常量有作用域,在一个函数中定义的const常量作用域只在该函数中。

bool

例子,下列cpp代码:

bool b = 0; //0, false
printf("b = %d\n", b); //0
b++; //1, true
printf("b = %d\n", b); //1
b = b - 3; //-2, true
printf("b = %d\n", b); //1

C中,三目运算符返回的是变量值,不能作为左值 C++中,三目运算符直接返回变量引用,既可以当左值,又可以做右值。但如果三目运算符可能返回的值中如果有一个是常量值,返回的是值,不能作为左值使用。

int a = 1;
int b = 2;

(a < b ? a : b) = 3;
//在c中,这是错的,因为三目运算符?:不能当左值
//在cpp中,a<b为true, 所以返回a, 这个语句相当于 a = 3

printf("a = %d, b = %d\n", a, b); //a=3, b=2

引用的本质


变量 变量是一段实际连续存储空间的别名。程序中通过变量来申请并命名存储空间。通过变量的名字可以使用存储空间。

引用 一段连续的存储空间不一定只有一个别名,引用可以看作一个已定义变量的别名。 语法: Type& name = var;

int a = 4;
int& b = a; //b为a的别名, 引用在定义时要初始化

b = 5; //操作b就是操作a

引用作为变量别名而存在, 在一些场合可以代替指针。引用相对于指针来说具有更好的可读性和实用性。

对比swap函数的实现

void swap(int& a, int& b){
	//函数中的引用参数不需要初始化,初始化在函数调用时完成
	//a,b为引用,所以调用时是swap(a, b);
	int tmp = a;
	a = b;
	b = t;
}

void swap(int* a, int* b){ //调用时是swap(&a, &b);
	//传入a, b的地址
	int t = *a;
	*a = *b;
	*b = t;
}

const引用 语法: const Type& name = var; , 让变量拥有只读属性

例子:

int a = 4;
const int& b = a; //引用,代表b是变量a的别名
int* p = (int*)&b; //指针p指向a的地址

b = 5; //Error, b有const修饰, (a,b是)只读变量
*p = 5; //正确,还是可以用指针修改变量a的值

当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名。

const int& b = 1; //ok
int* p = (int*)&b;
b = 5; //Error
*p = 5; // ok

引用是另一个变量的别名,那么引用本身有存储空间吗?有 (指针是一个变量,保存着内存地址,具有存储空间)

struct TRef{
	char& r;
};

int main(){
	char c = 'c';
	char& rc = c;
	TRef ref = {c};

	printf("%d\n", sizeof(char&)); //1
	printf("%d\n", sizeof(rc)); //sizeof(c) => 1
	printf("%d\n", sizeof(TRef)); //4, 指针也是占用4 bytes
	printf("%d\n", sizeof()); //sizof(c) => 1
}

实际上,引用在C++中的内部实现是一个指针常量,所以引用所占用的空间大小与指针相同。 因为引用只是一个别名,C++为了实用性而隐藏了引用的存储空间。

Type& name; //<=> Type* const name;