C语言系列-小知识点积累(一)
1. C/C++ 语言中,宏只是展开,不同于函数调用
#include<stdio.h>
#define POW(a) a * a
int main() {
int a = 10;
int b = 2;
int c = POW(a) / POW(b);
printf("%d", c);
return 0;
}
结果为 10 ∗ 10 / 2 ∗ 2 = 100 ≠ 25 10 * 10 / 2 * 2=100 \neq25 10∗10/2∗2=100=25
2. C/C++语言中的基本类型
笔者之前面试腾讯的时候,被问到C语言中基本类型占用的字节数。对于这个问题,在不同位数的编译器是不一样的,大致如下表所示
| 类型 | 32位编译器 | 64位编译器 |
|---|---|---|
| char | 1 | 1 |
| short int | 2 | 2 |
| int | 4 | 4 |
| long int | 4 | 8 |
| float | 4 | 4 |
| double | 8 | 8 |
| long long | 8 | 8 |
| 指针 | 4 | 8 |
测试程序如下:
#include<stdio.h>
#include<stdlib.h>
typedef struct A {
int *a;
char b;
float c;
} A;
void main() {
char a = '1';
printf("sizeof(char)=%d\n", sizeof(a));
short int b = 2;
printf("sizeof(short int)=%d\n", sizeof(b));
int c = 3;
printf("sizeof(int)=%d\n", sizeof(c));
long int d = 4;
printf("sizeof(long int)=%d\n", sizeof(d));
long long e = 5;
printf("sizeof(long long)=%d\n", sizeof(e));
float f = 6.0;
printf("sizeof(float)=%d\n", sizeof(f));
double g = 6.0;
printf("sizeof(double)=%d\n", sizeof(g));
char *h = &a;
long long *l = &e;
double *m = &g;
char **n = &h;
printf("sizeof(char *)=%d, sizeof(long logn *)=%d, sizeof(double *)=%d, sizeof(char **)=%d\n", sizeof(h), sizeof(l), sizeof(m), sizeof(n));
printf("sizeof(struct A)=%d\n", sizeof(A));
char str[] = "1234";
printf("sizeof(str)=%d\n", sizeof(str));
}
windows10 gcc版本(gcc.exe (x86_64-mcf-seh-rev0, Built by MinGW-Builds project) 14.2.0)运行截图如下:

但是在ubuntu20.04 上,运行结果有点差异

对于结构体取sizeof,因为结构体会有内存对齐的要求,比如下图的结构体,sizeof的值为16。按照顺序,a占用8个字节,b占用1个字节,c占用4个字节,但是b和c之间会有一段3字节的填充,因为要保证c的开始地址也是自身长度的整数倍,这样避免需要两次内存读取才能读到c。
typedef struct A {
int *a;
char b;
float c;
} A;
还有对于字符串取sizeof,值可能比字面上的字符数多一,因为还有一个隐含的结束字符’\0’。
3. const与define的比较
在C语言中,define定义的宏在预处理阶段直接展开,不做如何的类型安全检查,而const定义的常量在编译运行阶段使用,有类型安全检查,会分配内存,一般放在静态区。
4. 类的构造函数
C++的类的构造函数,没有返回值,函数名就是类名,不可以是虚函数,一般包括默认构造函数、一般构造函数、拷贝构造函数、转换构造函数和移动构造函数。
默认构造函数,如果没有手动实现一般构造函数,编译器将会自动生成一个默认的空的构造函数,如果没有手动实现一个拷贝构造函数,那么编译器将会自动生成一个默认的拷贝构造函数,但默认的拷贝构造函数对于元素都是浅拷贝,如果遇到指针成员,那么原对象和拷贝出来的新对象的这个指针成员会指向同一个块内存。
对于一般构造函数,可以重载,形参不一样。对于拷贝构造函数,传入的参数往往是const Demo &A形式,即一个常引用,引用可以避免函数传参拷贝开销,常引用可以保证拷贝构造函数内部不会修改传入的原对象参数。
装换构造函数,可以允许将一些基本类型转换为类类型,其中就是一个转换构造函数在发挥作用。
最后是移动构造函数,因为在Demo A=Demo()语句中,实际上会调用两个构造函数,一个是默认的或者一般构造函数构造一个匿名对象,然后调用拷贝构造函数,如果有个成员比较重量级,那么拷贝的开销是巨大的,所以就引入了移动构造函数,因为是匿名对象,不会被调用,那么可以直接将匿名对象的元素移动给新对象。
一个测试程序如下:
#include<iostream>
#include<chrono>
#include <cstring>
#define LENGTH 10000000
// g++ constructor.cpp -fno-elide-constructors -o demo
class Person {
private:
float weight;
double salary;
double *work_times;
public:
Person(float weight, double salary) { // 一般构造函数
this -> weight = weight;
this -> salary = salary;
this->work_times = new double[LENGTH];
std::cout << "general construtor called" << std::endl;
}
Person(float weight) { // 转换构造函数
this -> weight = weight;
this->work_times = new double[LENGTH];
std::cout << "transform constructor called" << std::endl;
}
Person(const Person &p) { // 拷贝构造函数
this -> weight = p.weight;
this -> salary = p.salary;
this->work_times = new double[LENGTH];
memcpy(this->work_times, p.work_times, sizeof(double) * LENGTH);
std::cout << "copy constructor called" << std::endl;
}
Person(Person &&p) { // 移动构造函数
this -> weight = p.weight;
this -> salary = p.salary;
this -> work_times = p.work_times;
p.work_times = NULL;
std::cout << "move constructor called" << std::endl;
}
};
int main() {
Person p1 = 81.9;
auto start = std::chrono::system_clock::now();
Person p2 = Person(78.1, 4500);
auto finish = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(finish-start);
std::cout << double(duration.count())<< "ms" <<std::endl;
return 0;
}
以下为运行截图,第一次注释掉移动构造函数,第二次去掉注释。

可以看到,创建变量的时候都是先调用转换构造函数或者一般构造函数构造一个匿名对象,然后有移动构造函数优先使用移动构造函数,因为开销小。此外,运行这种测试程序最好在linux上测试,在windows真的不知道会是什么样子。
5. C++: NULL和nullptr
在C语言里面只有NULL,标识一个值为0的void*指针,但是在C++里面,为了解决函数重载的二义性问题,将NULL定义为了整数0,同时在C++ 11中引入了nullptr标识空指针。
6. C++: 虚函数
首先是虚函数和纯虚函数的比较。虚函数定义为在类的成员函数前面加上一个virtual关键字,可以有实现,纯虚函数定义一般为virtual void work() = 0;,含有纯虚函数的类为抽象类,不能被实例化,且其子类必须实现这个纯虚函数。
虚函数是用来实现多态的,允许父类指针指向子类,然后通过指针调用一个函数,只能到运行期才知道调用的是那个函数,所以构造函数不能是虚函数,因为此时的类型还没有确定。但是析构函数可以是虚函数,一般也建议设置为虚函数,因为析构父类,如果父类的析构函数不是虚函数,那么只会调用父类的析构函数,子类的资源可能不会被正常清理。而如果父类的析构函数是虚函数,那么会先调用子类的析构函数,然后再调用父类的析构函数。如下为相关测试程序:
#include<iostream>
class Life {
private:
int age;
public:
virtual void work() = 0; // 纯虚函数定义
};
class Person: public Life {
private:
int age;
double salary;
public:
virtual void read() {
std::cout << "Person read" << std::endl;
}
virtual void write() {
std::cout << "Person write" << std::endl;
}
virtual ~Person() {
std::cout << "Person deleted" << std::endl;
}
void work() {
std::cout << "Person work" << std::endl;
}
};
class Man: public Person {
private:
int age;
double salary;
public:
void read() {
std::cout << "man read" << std::endl;
}
void write() {
std::cout << "man write" << std::endl;
}
~Man() {
std::cout << "man deleted" << std::endl;
}
void work() {
std::cout << "man work" << std::endl;
}
};
int main() {
Person *A = new Man();
delete(A);
return 0;
}
5万+

被折叠的 条评论
为什么被折叠?



