一,const的用法
1,修饰常量
使用const定义的常量在整个程序的生命周期都不会被改变,任何想尝试修改该常量的操作都会报错
const int a = 10;
int* x = &a; //错误
const int* y = &a;//对
另外const定义的常量必须要在声明时初始化
2,修饰指针
指向常量的指针
int a = 10;
int b = 10;
const int* ptr = &a; //这种情况const修饰的是ptr指向的值,而不是ptr本身
ptr = &b; //正确
*ptr = 20; //错误
此时可以修改指针指向的地址,但不能修改指向变量的值
常量指针
int a = 10;
int b = 10;
int* const ptr = &a; //这种情况const修饰的是ptr自身
ptr = &b; //错误
*ptr = 20; //正确
正好与上面相反
指向常量的常量指针
int a = 10;
int b = 10;
const int* const ptr = &a; //此时const分别修饰ptr和ptr指向的值,所以都不能修改
ptr = &b; //错
*ptr = 20; //错
总结 const是右结合的,const修饰的是离它最近的右边的部分
3,修饰函数参数
希望函数的参数在函数内部不被修改可以使用const修饰
void fun(const int a)
{
a++; //错误
printf("a=%d", &a);
}
4,修改函数返回值
通常用于返回引用或指针,防止被修改
const int* fun()
{
int a = 10;
return &a;
}
5,c++中还可以用来修饰成员函数,以表明该成员函数不会修改类的状态。通常,它用于那些只读取对象数据而不修改它们的成员函数,另外在该类实例化的对象,const修饰的只能使用const修饰的成员函数,不加的则无该限制
class MyClass {
public:
int getValue() const
{
return value;
}
void setValue(int val)
{
value = val;
}
private:
int value;
};
int main()
{
MyClass a;
MyClass const b;
a.setValue(10); //true
b.getValue(10); //false
a.getValue(); //true
b.getValue(); //true
}
二,枚举与define的区别
枚举enum和宏定义#define都是用于定义常量的方法
区别
1,语法和用法
enum Color {
RED, // 默认值 0
GREEN, // 默认值 1
BLUE // 默认值 2
};
//enum 有默认值每个枚举变量从0开始
enum Color myColor = RED;
std::cout << myColor << std::endl; //会打印RED的默认值0
#define RED 0
#define GREEN 1
#define BLUE 2
//define只是对文本做了简单替换,在编译阶段就进行替换
2,类型安全
enum 定义的是一个独立的类型,具有一定的类型安全性。
例如,你不能将 enum Color 类型的变量与另一个枚举类型或无关的整数进行赋值而不发生隐式转换。
虽然 enum 底层表示为整数,但编译器会检查类型一致性。
#define:
#define 定义的常量没有类型,它只是一个简单的文本替换。
这意味着你可以将宏定义的常量与任何类型进行混合操作,编译器不会进行类型检查。
3,作用域
enum基本只作用于它定义时的作用域,而#define是全局的
4,#define必须为每个常量指定一个值,而enum里会有从0开始的隐式自增
5,enum更容易读,更好的可维护性
6,enum:枚举类型的大小由编译器决定,通常是一个 int 的大小。
枚举值是常量,通常不占用实际内存。
#define:宏定义的常量也不占用内存
三,break,continue,return用法
1,break用于终止当前循环,或者switch语句,继续执行后面的程序
for (int i = 1; i < 10; i++)
{
if (i == 5)
{
break;
}
printf("%d ", i);
//打印 1 2 3 4
}
int num = 2;
switch (num)
{
case 1:
printf("1\n");
case 2:
printf("2\n");
break;
case 3:
printf("3\n");
default:
break;
}
//case2不加break输出2,3 加了输出2
2,continue
for (int i = 1; i < 10; i++)
{
if (i == 5)
{
continue;
}
printf("%d ", i);
}
//打印 1 2 3 4 6 7 8 9
用于跳出当前循环中的剩余代码,直接开始下一轮循环
int i = 0;
while (i < 10) {
i++;
if (i % 2 == 0) {
continue; // 跳过当前迭代
}
printf("%d\n", i);
}
// 输出 1 3 5 7 9
3,return语句
用于中止函数的执行,在有返回类型的函数还会返回一个值
int add(int a, int b) {
return a + b; // 返回两个整数的和
}
int main() {
int result = add(3, 4);
printf("Result: %d\n", result); // 输出 Result: 7
return 0;
}
main函数中,表示程序的退出状态码,0表示正常退出
四,static的用法
1,修饰局部变量,用static修饰的局部变量仅会被初始化一次,以后每次调用该函数时,static变量都会保持上一次的值
void func()
{
static int num=0;
num++;
printf("%d\n", num);
}
int main()
{
func();
func();
//输出1 2
}
作用域仅限制在定义它的函数,生命周期是程序的整个运行过程
2,静态全局变量
作用域在定义该常量的文件里,其他文件不可访问,但可以通过函数访问
//文件1
#include<iostream>
static int num = 10;
int GetNum()
{
return num;
}
void SetNum()
{
num++;
}
//文件2
#include<iostream>
#include<stdio.h>
extern int GetNum();
extern int SetNum();
int main()
{
SetNum();
printf("%d", GetNum());
}
3,修饰函数
与静态全局变量一样,只能在声明它的源文件调用
4,c++中,static可以修饰成员变量或者成员函数
静态成员变量不属于某个对象,而是属于这个类,所有对象共享静态成员变量,静态成员函数也是如此,此外静态成员变量不能在类中初始化
class A {
public:
static int a ;
int b = 0;
static void add()
{
a++;
//b++; //静态成员函数只能访问静态成员变量,这会报错
}
};
int A::a = 1;
int main()
{
A a;
a.add();
a.add();
std::cout << a.a << std::endl;
A::add(); //静态成员函数可以直接通过类名访问
std::cout << a.a << std::endl;
}
五,全局变量与局部变量的区别
1,作用域
全局变量在整个程序都可用,一般写在文件头部,在所有函数,文件都可用,除非使用static限制其在定义的源文件
局部变量的作用域在定义它的函数或代码块里
2,生命周期
全局变量的生命周期贯穿程序,在程序开始运行时分配内存,程序结束时释放内存
局部变量的声明周期是随着定义它的函数或者代码块被调用开始分配内存,调用结束释放内存
3,内存位置
全局变量被存放在数据段中,它们在程序的整个运行过程都存放在内存
局部变量被存放在栈中,,函数调用结束后,它们的内存被释放
4,初始化
全局变量如果没有手动赋值,它们一般会被初始化为0(整数),nullptr(指针)
局部变量不会被自动初始化
5,访问方法
全局变量可以在所有函数访问,在别的文件里用extern关键字访问
外部函数无法直接访问局部变量
六,如何延长一个局部变量的生存周期?
1,使用static关键字,可以让局部变量的生存周期延长到和程序一样
2,在堆上动态开辟内存,使用malloc或new在堆上开辟内存,创建一个局部变量的指针,即使函数调用结束,仍然可以通过该指针访问,直到手动释放
void useLocalVariable() {
int localVar = 42; // 普通局部变量
std::cout << "Inside useLocalVariable(): localVar = " << localVar << std::endl;
}
int main() {
useLocalVariable();
// localVar 在 useLocalVariable() 结束时失效
// std::cout << "Accessing localVar in main: " << localVar << std::endl; // 错误:localVar 不可访问
return 0;
}
int* allocateMemory() {
int* p = (int*)malloc(sizeof(int)); // 动态分配内存
if (p == nullptr) {
std::cerr << "Memory allocation failed" << std::endl;
return nullptr;
}
*p = 42; // 给分配的内存赋值
return p; // 返回指向内存的指针
}
int main() {
int* p = allocateMemory(); // 获取指向动态分配内存的指针
if (p != nullptr) {
std::cout << "Inside main(): *p = " << *p << std::endl; // 访问动态分配的内存
free(p); // 释放动态分配的内存
} else {
std::cerr << "Failed to allocate memory" << std::endl;
}
return 0;
}
3,返回局部变量的地址
七,sizeof与strlen区别
sizeof是一个运算符,而strlen是一个关键字,sizeof主要用于计算一个对象或者数据类型在内存中所占据的字节数,包括内置类型,指针,引用类型,或者结构体这种自定义的数据类
strlen是一个函数,主要计算一个字符串的长度,不包括‘/0’终止符
都返回一个size_t类型的值
八,标识符命名规则
- 标识符必须以字母(
A-Z
或a-z
)或下划线(_
)开头。 - 后续字符可以是字母、数字(
0-9
)、或下划线(_
)。 - 标识符对大小写敏感。例如,
myVar
和MyVar
是两个不同的标识符。
九,#include <filename.h>和 #include “filename.h” 有什么区别
#include <filename.h>主要用于包含系统头文件或标准库文件,编译器会首先从标准库目录和系统库目录中查询
#include"filename"主要用于包含自定义的头文件,编译器首先从当前目录查询
十,字符数组与字符指针的区别
1,内存分配
字符数组是在栈上开辟内存的,随程序的结束而释放,大小在程序编译时确定,不会再改变,一般是以'\0'结尾,数组元素的内存都是连续的,数组名是指向数组首元素的指针,它的地址不会变
字符指针如果指向常量,则是在全局/数据区,如果指向变量,一般指向堆上动态开辟的内存
2,字符数组的大小不可变,内容可以改变,字符指针指向的地址可以改变(如果指向的不是常量)
3,字符数组的生命周期随程序结束,字符指针则需要手动回收
十一,大小端存储
是指计算机系统中多字节数据(如整数、浮点数等)在内存中存储时的字节顺序。不同的计算机系统可能采用不同的字节序
大端存储,数据的高位字节存储在低地址,低位字节存储在高地址
小段存储相反
判断方式
联合体,联合体union允许不同数据类型共享同一块内存
union U {
int i;
char c[4];
};
int main()
{
U u;
u.i = 0x01234567;
std::cout <<(int) (unsigned char)u.c[0] << std::endl;
}
//小端存储
大小端主要与操作系统,计算机架构有关,x86,64基本都是小端存储
十二,函数指针与指针函数
函数指针就是一个指向函数的指针,通过这个指针可以调用函数,在回调函数机制中非常有用,可以将函数作为参数传递
int add(int a, int b) { return a + b; }
int main()
{
int (*pf)(int, int) = add;
std::cout << pf(1, 2) << std::endl;
}
指针函数就是一个返回类型为指针的函数
int * GetInt()
{
int* p = new int(10);
return p;
}
int main()
{
int* p = GetInt();
std::cout << *p << std::endl;
}
十三,回调函数
回调函数是用函数指针来实现的一个编程模式,将函数作为参数传递给另一个函数
typedef int (*PF)(int, int);
//定义回调函数
void test(int a,int b,PF pf)
{
std::cout << pf(a, b) << std::endl;
}
//传递回调函数作为参数
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main()
{
test(1, 2, add);
test(3, 4, multiply);
//调用
}
回调函数的工作原理
-
定义回调函数: 定义一个函数,其签名与调用者期望的回调函数签名相匹配。
-
传递回调函数: 将回调函数的指针作为参数传递给另一个函数。
-
调用回调函数: 在被调用函数的某个时刻,根据需要调用传递过来的回调函数。
十四,void指针
void
指针(即 void*
指针)是一种通用指针类型,它可以指向任意类型的数据。由于 void
本身不指定具体的数据类型,所以 void*
指针不带有任何与类型相关的信息。这使得 void*
指针在需要通用性或处理不同类型数据的情况下非常有用
1,不能直接解引用,在解引用前必须要转换类型
2,void*在内存管理的函数中使用(malloc,realloc),可以转换为任意的数据类型
用void*指针实现通用链表
#include <iostream>
#include <cstdlib>
// 链表节点
struct Node {
void* data;
Node* next;
};
// 添加节点
void appendNode(Node** head, void* new_data, size_t data_size) {
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = malloc(data_size);
new_node->next = nullptr;
std::memcpy(new_node->data, new_data, data_size);
if (*head == nullptr) {
*head = new_node;
} else {
Node* temp = *head;
while (temp->next != nullptr) {
temp = temp->next;
}
temp->next = new_node;
}
}
// 打印整数链表
void printList(Node* node, void (*fptr)(void*)) {
while (node != nullptr) {
fptr(node->data);
node = node->next;
}
std::cout << std::endl;
}
void printInt(void* data) {
std::cout << *(int*)data << " ";
}
int main() {
Node* start = nullptr;
int arr[] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
appendNode(&start, &arr[i], sizeof(int));
}
printList(start, printInt); // 输出:10 20 30 40 50
return 0;
}
十五,值传递和地址传递
值传递实际接收的是实际参数的副本,在函数里对它做任何操作都不会影响实际参数,函数调用时会将实际参数的值复制一份给形参,因此它的缺点就是复制过程可能会消耗很多实践
地址传递接收的是实际参数的地址,函数可以直接访问实参并对它修改,无需复制
//地址传递
void func(int* a)
{
*a = 10;
}
//值传递
void func2(int a)
{
a = 20;
}
int main()
{
int a = 5;
func(&a);
std::cout << "a=" << a << std::endl; // 输出:a=10
int b=5;
func2(b);
std::cout << "b=" << b << std::endl; // 输出:b=5
}