【JNI】C,CPP概念及使用方法
. ->运算符
在C和C++两种语言中,.
和->
运算符都存在,并且它们的用法和含义是相同的。
.
运算符:在C和C++中,当你有一个结构体或类的实例(也就是一个对象)时,你可以使用.
运算符来访问其成员。->
运算符:在C和C++中,当你有一个指向结构体或类的指针时,你可以使用->
运算符来访问其成员。
JNIEnv *env;
c中你需要写(*env)->member
,而在C++中,你可以简单地写env->member
是因为JNIEnv在c和cpp是不一样的,
对于(env) c中是JNINativeInterface,cpp是_JNIEnv。并不是由于. ->运算符在c和cpp中的功能不同
方法签名
JNIEnv *env, jobject thiz
参数签名时要省略
例如:
jint native_op(JNIEnv *env, jobject thiz,int p) {
return p;
}
对应的签名是:(I)I
函数的类型是什么
还是上面那个例子:
jint native_op(JNIEnv *env, jobject thiz,int p) {
return p;
}
如果只写一个native_op
他的类型应该怎么写?
int addFunction(int a, int b) {
return a + b;
}
void test(){
int (*funcPtr)(int, int) = addFunction;
int p = funcPtr(1,2);
funcPtr(p,3);
}
对于addFunction
,他的类型是int (*funcPtr)(int, int)
,变量名字叫funcPtr
所以例子中的函数类型是:jint (*funcPtr)(JNIEnv *, jobject, int)
当然在c++中除了上面那样写,还可以使用auto
关键字,c语言中的auto没有这个特性
void test() {
auto funcPtr = addFunction;
int p = funcPtr(1, 2);
funcPtr(p, 3);
}
c++特有的类型自动推断
在C++中,auto
关键字用于自动类型推断,让编译器根据初始化表达式来推断变量的类型。这在处理复杂类型或者想要避免类型名称冗长的情况下非常有用。以下是一个简单的例子:
auto i = 5; // i is int
auto d = 3.14; // d is double
auto s = "hello"; // s is const char*
在这个例子中,编译器会根据等号右边的值来推断i
、d
和s
的类型。这样可以使代码更简洁,更易于阅读和维护。
注: 在C语言中,
auto
关键字主要用于声明一个变量为自动存储类型,这意味着这个变量的生命周期仅限于它的声明块(通常是一个函数)。一旦控制流离开这个块,变量就会被销毁。然而,实际上
auto
关键字在C语言中的使用非常少,因为如果没有明确指定存储类型,局部变量默认就是自动存储类型。也就是说,以下两个声明是等价的:int x; // 默认为自动存储类型 auto int y; // 显式声明为自动存储类型
因此,
auto
关键字在C语言中的应用场景非常有限,通常不需要显式使用。在现代C++中,auto
关键字的含义已经改变,用于自动类型推断,这是一个完全不同的概念。
c++中的lambda
除了和c语言一样去定义函数
还可以使用lambda
int main() {
// 定义一个lambda表达式,接受两个int参数,返回它们的和
auto add = [](int a, int b) {
return a + b;
};
// 使用lambda表达式
int result = add(3, 4);
// result is 7
std::cout << result << std::endl;
return 0;
}
lambda表达式的[]
被称为捕获说明符(capture specifier)。它用于指定lambda表达式可以访问哪些在其外部作用域中定义的变量,以及如何访问这些变量。以下是一些基本的捕获说明符:
[]
:空捕获列表。这意味着lambda表达式不能访问任何外部作用域的变量。[=]
:值捕获。这意味着lambda表达式可以访问所有在其外部作用域中定义的变量,但是它获取的是这些变量的副本,而不是变量本身。因此,lambda表达式不能修改这些变量的值。[&]
:引用捕获。这意味着lambda表达式可以访问所有在其外部作用域中定义的变量,并且它获取的是这些变量的引用。因此,lambda表达式可以修改这些变量的值。[var]
或[&var]
:单个变量的值捕获或引用捕获。这意味着lambda表达式只能访问指定的变量,方式可以是值捕获或引用捕获。[=, &var]
或[&, var]
:混合捕获。这意味着lambda表达式可以按值捕获所有变量,但按引用捕获var
,或者按引用捕获所有变量,但按值捕获var
。
这些捕获说明符可以让你更灵活地控制lambda表达式的行为,使其能够适应各种不同的编程需求。
c++ 面向对象
在C++中,对象是类的实例。首先,你需要定义一个类,然后你可以创建该类的对象。以下是一个简单的例子:
#include <iostream>
// 定义一个类
class MyClass {
public:
int myNumber;
std::string myString;
private:
int privateNumber;
public:
void print() {
std::cout << "Number: " << myNumber << ", String: " << myString << std::endl;
}
};
int main() {
// 创建一个MyClass的对象
MyClass obj;
// 设置对象的属性
obj.myNumber = 5;
obj.myString = "Hello";
// 调用对象的成员函数
obj.print();
return 0;
}
在这个例子中,MyClass
是一个类,它有两个成员变量(myNumber
和myString
)和一个成员函数(print
)。然后,我们在main
函数中创建了一个MyClass
的对象obj
,设置了它的属性,并调用了它的成员函数。
注: 在C++中,创建对象的方式有两种:一种是在栈上创建,另一种是在堆上创建。
在上述例子中,我们在栈上创建了一个对象。这种情况下,不需要使用
new
关键字。对象在声明时就已经创建,并且在离开作用域时会自动销毁。MyClass obj; // 在栈上创建对象
如果你想在堆上创建对象,那么就需要使用
new
关键字。这种情况下,对象在使用new
创建后存在,直到你使用delete
显式销毁它。MyClass* obj = new MyClass(); // 在堆上创建对象 // 使用对象... delete obj; // 销毁对象
在堆上创建对象可以让对象的生命周期超过创建它的作用域,但需要手动管理内存,否则可能会导致内存泄漏。
构造方法
在C++中,构造函数是一种特殊的成员函数,它在创建类的对象时自动调用。构造函数的名称与类的名称相同,没有返回类型。以下是一些构造函数的写法:
- 默认构造函数:没有参数或所有参数都有默认值。
class MyClass {
public:
MyClass() {
// 默认构造函数
}
};
- 参数化构造函数:带有参数的构造函数。
class MyClass {
public:
int x;
MyClass(int val) : x(val) {
// 参数化构造函数
}
};
- 拷贝构造函数:用于初始化一个对象为另一个对象的副本。
class MyClass {
public:
int x;
MyClass(const MyClass &obj) : x(obj.x) {
// 拷贝构造函数
}
};
- 移动构造函数:用于初始化一个对象为另一个对象的"移动"副本。
class MyClass {
public:
int *x;
MyClass(MyClass &&obj) : x(obj.x) {
obj.x = nullptr; // 移动构造函数
}
};
- 委托构造函数:一个构造函数调用同类的其他构造函数。
class MyClass {
public:
int x, y;
MyClass(int val) : x(val), y(val) {}
MyClass() : MyClass(0) {} // 委托构造函数
};
在C++中,x(val)
和y(val)
是成员初始化列表的一部分,用于初始化类的成员变量。在这个例子中,x(val)
和y(val)
表示将val
的值分别赋给x
和y
。
成员初始化列表位于构造函数参数列表和函数体之间,由冒号开始,每个初始化器由一个成员名和括号内的初始值组成,初始化器之间用逗号分隔。
这种初始化方式比在构造函数体内部进行赋值更有效,因为它可以直接初始化成员,而不是先创建然后再赋值。对于某些类型的成员(如const或引用成员),必须在成员初始化列表中进行初始化,因为它们不能在构造函数体内部赋值。
如果你既要初始化成员变量又要调用基类的构造函数,你可以在派生类的构造函数中使用成员初始化列表。成员初始化列表可以同时完成这两个任务。以下是一个示例:
#include <iostream>
class Base {
public:
Base(int x) {
std::cout << "Base constructor called with value: " << x << std::endl;
}
};
class Derived : public Base {
public:
int y;
Derived(int x, int y_value) : Base(x), y(y_value) { // 调用基类的构造函数并初始化成员变量
std::cout << "Derived constructor called with value: " << y << std::endl;
}
};
int main() {
Derived obj(10, 20);
}
在这个例子中,Derived
类的构造函数通过成员初始化列表调用了Base
类的构造函数,并初始化了成员变量y
。当创建Derived
类的对象时,将首先调用Base
类的构造函数,然后初始化y
,最后执行Derived
类的构造函数体。
注意:
在 C++ 中,你可以使用括号来调用带有参数的构造函数,例如
Derived deri(1, 2, 3);
。但是,如果你想调用默认构造函数(即不带参数的构造函数),你应该避免使用括号,因为Derived deri();
会被解析为一个函数声明,而不是对象定义。所以,如果你想调用默认构造函数来创建对象,你应该使用以下两种方式之一,而无法使用
Derived deri();
:
Derived deri;
Derived deri{};
这两种方式都会调用
Derived
类的默认构造函数来创建deri
对象。这是因为 C++ 的语法规则中有一个被称为 "最多吃原则"(most vexing parse),即在解析语句时,如果某种解析方式使得该语句成为一个合法的声明,那么编译器就会优先采用这种解析方式。这就是为什么
Derived deri();
会被解析为函数声明的原因。例如,如果
Derived
类有一个接受两个int
参数的构造函数,你可以这样初始化deri
:Derived deri{1, 2};
如果
Derived
类有一个默认构造函数,你可以这样初始化deri
:Derived deri{};
这种语法在 C++11 及以后的版本中都是有效的。
- 窄化转换:使用
{}
初始化列表的方式会阻止窄化转换。窄化转换是指一些可能导致数据丢失或者改变的类型转换,例如从double
到int
,或者从int
到char
。如果你试图用一个可能导致窄化转换的值去初始化一个变量,编译器会给出警告或错误。例如,Derived deri{1.0, 2.0};
可能会编译失败,而Derived deri(1.0, 2.0);
则可能会成功,但可能导致数据丢失。- 最优匹配:如果
Derived
类有一个接受std::initializer_list<int>
的构造函数,那么Derived deri{1, 2};
会优先调用这个构造函数,而Derived deri(1, 2);
则会调用接受两个int
参数的构造函数。
覆盖new,设置自己的内存分配规则
在C++中,operator new
是一个特殊的运算符函数,它被用来处理动态内存分配。当你使用 new
关键字来创建对象时,operator new
被调用来分配足够的内存来存储对象。
例如,当你执行如下代码:
MyClass* myObject = new MyClass();
C++编译器会转换这个 new
表达式为两个主要步骤:
- 调用
operator new
函数来分配足够的内存来存储MyClass
类型的对象。这个函数的参数是要分配的内存大小,这个大小通常由编译器计算得出。 - 在分配的内存上调用
MyClass
的构造函数来初始化对象。
operator new
函数可以被重载,这意味着你可以为你的类定义自己的内存分配逻辑。例如:
void* operator new(size_t size) {
// 自定义内存分配逻辑
void* p = malloc(size);
// 处理分配失败的情况
if (!p) throw std::bad_alloc();
return p;
}
void operator delete(void* p) {
// 自定义内存释放逻辑
free(p);
}
上面的代码定义了一个全局的 operator new
和 operator delete
函数,它们分别用于分配和释放内存。
在C++中,operator new
通常不直接调用。它是由 new
表达式隐式调用的,正如前面例子所示。然而,如果你需要直接调用 operator new
,你可以这样做:
void* memory = operator new(sizeof(MyClass));
这将分配足够的内存来存储 MyClass
类型的对象,但不会调用构造函数。这种方式通常用在高级场合,比如自定义内存管理器或实现placement new等。记住,如果你这样直接分配内存,你需要手动调用对象的构造函数(通过placement new)并最终调用析构函数和 operator delete
来正确地释放内存。
派生
C++中,派生是通过继承来创建新类的过程。新类(派生类)继承了一个或多个现有类(基类)的特性。以下是一个简单的例子:
// 基类
class BaseClass {
public:
int baseNumber;
void basePrint() {
std::cout << "Base Number: " << baseNumber << std::endl;
}
};
// 派生类
class DerivedClass : public BaseClass {//还有private、protected
public:
int derivedNumber;
void derivedPrint() {
std::cout << "Derived Number: " << derivedNumber << std::endl;
}
};
int main() {
DerivedClass obj;
// 访问基类的成员
obj.baseNumber = 10;
obj.basePrint();
// 访问派生类的成员
obj.derivedNumber = 20;
obj.derivedPrint();
return 0;
}
在这个例子中,DerivedClass
是通过公有继承从BaseClass
派生的。这意味着BaseClass
的所有公有和保护成员都成为了DerivedClass
的成员,并且保持了它们在BaseClass
中的访问级别。因此,我们可以在DerivedClass
的对象中访问baseNumber
和basePrint
。如果选择私有继承,那么
BaseClass对外是不可见的,只可以在DerivedClass类中调用。
C++ 模板 也就是java和kotlin里面的泛型
在C++中,模板是一种特性,允许你编写通用的代码,可以处理多种数据类型。模板可以应用于函数(称为函数模板)和类(称为类模板)。
以下是一个函数模板的例子:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max<int>(3, 7) << std::endl; // 输出7
std::cout << max<double>(3.14, 2.72) << std::endl; // 输出3.14
return 0;
}
这里的max<int>(3, 7)
等价于max(3, 7)
,c++可以自动推断出。
在这个例子中,max
函数是一个模板函数,可以接受任何类型的参数,只要该类型支持>
运算符。
以下是一个类模板的例子:
template <typename T>
class MyPair {
public:
T first, second;
MyPair(T a, T b) {
first = a;
second = b;
}
T getMax() {
return (first > second) ? first : second;
}
};
int main() {
MyPair<int> intPair(3, 7);
std::cout << intPair.getMax() << std::endl; // 输出7
MyPair<double> doublePair(3.14, 2.72);
std::cout << doublePair.getMax() << std::endl; // 输出3.14
return 0;
}
在这个例子中,MyPair
类是一个模板类,可以接受任何类型的参数,只要该类型支持>
运算符。
c++ 异常处理
在C++中,异常处理主要涉及到以下几个关键字:try
,catch
,throw
。以下是一些基本的使用方式:
- 抛出异常:使用
throw
关键字抛出异常。
throw "An error occurred";
- 捕获异常:使用
try
/catch
块捕获并处理异常。
try {
// 可能抛出异常的代码
throw "An error occurred";
}
catch (const char* msg) {
std::cerr << msg << std::endl;
}
- 捕获所有异常:使用
catch(...)
捕获所有类型的异常。
try {
// 可能抛出异常的代码
throw "An error occurred";
}
catch (...) {
std::cerr << "An unknown error occurred" << std::endl;
}
- 标准库异常:C++标准库提供了一系列的异常类,如
std::exception
,std::runtime_error
,std::out_of_range
等。
try {
// 可能抛出异常的代码
throw std::runtime_error("A runtime error occurred");
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
请注意,异常处理应该用于处理那些无法预防或无法通过其他方式处理的错误情况。过度使用异常处理可能会导致代码难以理解和维护。
c++ 可变参数
在C++中,...
被称为省略号或者可变参数。它有两个主要的用途:
- 在函数参数列表中表示该函数可以接受任意数量和类型的参数。这在C++的旧版本中常见,但在现代C++中,我们更倾向于使用模板和容器来处理可变数量的参数。
#include <cstdarg>
#include <iostream>
void printNumbers(int count, ...) {
va_list list;
va_start(list, count);
for (int i = 0; i < count; ++i) {
std::cout << va_arg(list, int) << '\n';
}
va_end(list);
}
int main() {
printNumbers(3, 10, 20, 30);
}
- 在
catch
语句中,...
表示捕获所有类型的异常。
try {
// 可能抛出异常的代码
throw "An error occurred";
}
catch (...) {
std::cerr << "An unknown error occurred" << std::endl;
}
在C++11及其后续版本中,...
还用于模板编程中的参数包,用于表示任意数量和类型的模板参数。
例如:
在C++11及其后续版本中,...
被用于模板编程中的参数包,表示任意数量和类型的模板参数。这是一种强大的功能,允许你创建可以处理任意类型和数量参数的模板函数或模板类。
以下是一个简单的例子,展示了如何使用参数包创建一个可以接受任意数量参数的函数模板:
#include <iostream>
// 基本情况,没有参数
void print() {}
// 递归模板函数
template<typename T, typename... Args>
void print(T head, Args... tail) {
std::cout << head << " ";
print(tail...); // 递归调用,参数包中的下一个参数成为新的head
}
int main() {
print(1, 2.0, "three", '4');//省略泛型的声明<>
}
在这个例子中,print
函数模板可以接受任意数量和类型的参数。T
表示第一个参数的类型,Args...
表示剩余参数的类型。在函数体中,print(tail...)
递归地调用print
函数,每次调用都将参数包中的下一个参数作为head
。
va_start
这里的va_start
是C和C++中处理可变参数的宏。它被用于初始化va_list
类型的对象,这个对象用于访问函数参数列表中的可变参数。
在使用va_start
宏之前,你需要定义一个va_list
类型的变量,这个变量将被初始化为指向第一个可变参数的指针。va_start
接受两个参数:第一个是刚刚提到的va_list
变量,第二个是可变参数列表前的最后一个非可变参数。
以下是一个例子:
#include <cstdarg>
#include <iostream>
void printNumbers(int count, ...) {
va_list list;
va_start(list, count); // 初始化va_list
for (int i = 0; i < count; ++i) {
int num = va_arg(list, int); // 获取下一个参数的值
std::cout << num << '\n';
}
va_end(list); // 清理为va_list分配的内存
}
int main() {
printNumbers(3, 10, 20, 30);
}
在这个例子中,va_start(list, count)
将list
初始化为指向第一个可变参数的指针。然后,va_arg
宏被用于获取可变参数的值。最后,va_end
宏被用于清理为va_list
分配的内存。
c++ 智能指针
在C++中,智能指针是一种对象,它可以像常规指针一样处理,但当它不再需要时,它会自动删除它所指向的对象。这种自动删除可以防止内存泄漏,使内存管理变得更加简单。
C++标准库提供了几种类型的智能指针,包括std::unique_ptr
,std::shared_ptr
和std::weak_ptr
。
std::unique_ptr
是一种独占所有权的智能指针,它不允许多个指针指向同一个对象。std::shared_ptr
允许多个指针共享同一个对象,当最后一个shared_ptr
不再指向对象时,对象会被删除。std::weak_ptr
是一种不控制对象生命周期的智能指针,它被设计为与std::shared_ptr
一起使用,防止智能指针的循环引用。
使用智能指针可以帮助你更有效地管理内存,避免常规指针可能导致的内存泄漏和悬挂指针问题。
当然可以。以下是一个使用std::unique_ptr
的例子:
#include <memory>
struct MyClass {
int value;
MyClass(int v) : value(v) {}
};
int main() {
std::unique_ptr<MyClass> ptr(new MyClass(10));
std::cout << ptr->value << std::endl; // 输出:10
return 0;
}
在这个例子中,当ptr
离开其作用域(即main
函数结束)时,它会自动删除它所指向的MyClass
对象,释放内存。
以下是一个使用std::shared_ptr
的例子:
#include <memory>
struct MyClass {
int value;
MyClass(int v) : value(v) {}
};
int main() {
std::shared_ptr<MyClass> ptr1(new MyClass(10));
{
std::shared_ptr<MyClass> ptr2 = ptr1;
std::cout << ptr1->value << std::endl; // 输出:10
std::cout << ptr2->value << std::endl; // 输出:10
} // ptr2离开作用域,但ptr1仍然指向对象
std::cout << ptr1->value << std::endl; // 输出:10
return 0;
}
在这个例子中,ptr1
和ptr2
共享同一个对象。当ptr2
离开其作用域时,对象不会被删除,因为ptr1
仍然指向它。只有当ptr1
也离开其作用域时,对象才会被删除。
验证:std::unique_ptr
指向的指针只能存在一个引用,编译期间就会报错
c++ 并发
以下是一些C++并发编程的例子:
- 线程(Threads) :
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int main() {
std::thread t(hello);
t.join();
return 0;
}
- 互斥量(Mutexes) :
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_block(int n, char c) {
mtx.lock();
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock();
}
int main() {
std::thread th1(print_block,50,'*');
std::thread th2(print_block,50,'$');
th1.join();
th2.join();
return 0;
}
- 条件变量(Condition Variables) :
#include <iostream>
#include <thread>
#include <condition_variable>
std::condition_variable cv;
std::mutex cv_m;
int i = 0;
void waits() {
std::unique_lock<std::mutex> lk(cv_m);
cv.wait(lk, []{return i == 1;});
std::cout << "Finished waiting. i == 1\n";
}
void signals() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Notifying...\n";
i = 1;
cv.notify_one();
}
int main() {
std::thread t1(waits), t2(signals);
t1.join();
t2.join();
return 0;
}
std::this_thread
是C++标准库中的一个命名空间,它提供了一些函数,这些函数对于操作当前线程非常有用。这些函数包括:
std::this_thread::get_id()
:返回当前线程的线程ID。std::this_thread::sleep_for()
:使当前线程休眠指定的时间段。std::this_thread::sleep_until()
:使当前线程休眠直到指定的时间点。std::this_thread::yield()
:建议操作系统切换到其他线程。这是一种对操作系统的提示,表示当前线程没有紧急的任务要执行,可以切换到其他线程。
例如,以下代码使当前线程休眠1秒:
#include <iostream>
#include <thread>
#include <chrono>
int main() {
std::cout << "Sleeping for 1 second...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Awake!\n";
return 0;
}
这些函数可以帮助你更好地控制和管理线程的行为。
命名空间
命名空间(Namespace)是C++中的一个特性,用于组织代码并防止命名冲突。命名空间可以包含变量、函数、类等。
以下是一个简单的命名空间示例:
namespace MyNamespace {
int myVar = 10;
void myFunction() {
// 函数实现
}
}
你可以使用::
运算符来访问命名空间中的元素,如MyNamespace::myVar
。
命名空间的使用方式和应用场景包括:
- 防止命名冲突:当两个库都定义了同名的函数或类时,可以通过命名空间来区分它们。
- 代码组织:可以使用命名空间来将相关的函数、类和变量组织在一起。
- 使用
using
声明:可以使用using
声明来引入命名空间中的特定元素,或者使用using namespace
来引入整个命名空间。
例如:
using MyNamespace::myVar; // 引入特定元素
using namespace MyNamespace; // 引入整个命名空间
使用using namespace
后就不需要带MyNamespace::
前缀了
请注意,过度使用using namespace
可能会导致命名冲突,因此在编写大型项目时应谨慎使用。
JNI 资源释放
通过NewStringUTF
创建的对象不需要释放
也没有相关的释放函数
NewStringUTF
创建的jstring
对象是在Java堆上,由Java的垃圾收集器管理。当这个对象不再被使用时,垃圾收集器会自动回收它。因此,你不需要(也不能)在本地代码中手动释放这个对象。
这是Java和C++的一个主要区别。在C++中,你需要手动管理内存,包括创建和销毁对象。但在Java中,内存管理是自动的,由垃圾收集器负责。
所以,你在JNI代码中创建的jstring
对象,不需要手动释放。只要你正确地返回这个对象给Java代码,Java的垃圾收集器就会在适当的时候自动回收这个对象。
通过GetStringChars
或GetStringUTFChars
获取的字符串需要释放
因为他可能
会在本地内存创建char数组
,超出了java管理范围。
GetStringChars
函数在JNI中被用来获取Java字符串的Unicode字符数组。这个函数返回一个指向字符数组的指针,这个数组可能是原始字符串的副本,也可能是直接指向原始字符串的指针。
如果返回的是副本,那么这个副本是在本地内存中创建的,不会被Java的垃圾收集器管理。因此,你需要调用ReleaseStringChars
函数来释放这个副本,防止内存泄漏。
如果返回的是直接指向原始字符串的指针,那么调用ReleaseStringChars
函数可以通知JVM你已经完成了对这个字符串的操作,JVM可以自由地移动或者回收这个字符串。
总的来说,无论GetStringChars
返回的是副本还是直接的指针,你都应该在完成操作后调用ReleaseStringChars
函数,以确保资源被正确管理,防止内存泄漏或其他问题。
其他
在JNI中,主要需要手动释放的资源有以下几种:
- 本地引用(Local References):JNI函数经常会返回本地引用,这些引用在方法返回后会自动被释放。但是如果你在一个方法中创建了大量的本地引用,可能会耗尽本地引用表的空间,导致OutOfMemoryError。在这种情况下,你可以调用
DeleteLocalRef
函数手动释放本地引用。 - 全局引用(Global References):全局引用不会被自动释放,需要你手动调用
DeleteGlobalRef
函数释放。如果你不释放全局引用,可能会导致内存泄漏。 - 弱全局引用(Weak Global References):弱全局引用也需要你手动调用
DeleteWeakGlobalRef
函数释放。 - 通过
GetStringChars
或GetStringUTFChars
获取的字符串:这些函数返回的字符数组可能是复制的,需要你调用ReleaseStringChars
或ReleaseStringUTFChars
函数释放。 - 通过
GetBooleanArrayElements
、GetByteArrayElements
、GetCharArrayElements
等函数获取的数组元素:这些函数返回的数组元素可能是复制的,需要你调用相应的Release<Type>ArrayElements
函数释放。 - 通过
GetPrimitiveArrayCritical
获取的数组元素:这个函数返回的数组元素可能是复制的,需要你调用ReleasePrimitiveArrayCritical
函数释放。
需要注意的是,JNI的这些"释放"函数并不总是真正释放内存。在某些情况下,它们可能只是通知JVM你已经完成了对某个对象的操作,而JVM会在适当的时候自动回收这个对象(比如GetStringChars
指向原始字符串地址,没有创建新的char数组)。但是,如果你不调用这些函数,可能会导致内存泄漏或其他问题。
静态绑定 动态绑定
静态绑定
class Base {
public:
void display() { std::cout << "Base::display\n"; }
};
class Derived : public Base {
public:
void display() { std::cout << "Derived::display\n"; }
};
int main() {
Derived d;
Base* ptr = &d;
ptr->display(); // 输出 "Base::display"
}
class Base {
public:
void display();
};
class Derived : public Base {
public:
void display() { std::cout << "Derived::display\n"; }
};
int main() {
Derived d;
Base* ptr = &d;
ptr->display(); // 输出 "Base::display"
}
上面两种都是静态绑定
也就是说我的Derived转换为基类Base时,调用重名方法也会调用Base中的display,但是如果是d.display(),就会输出Derived中定义的方法。
通过Base::display实现方法
上面的第二个例子Base虽然没有实现该方法,但是如果你代码中存在像ptr->display()这种对Base.display方法的引用,那么编译会失败,提示找不到该方法。假如改写成这样:
class Base {
public:
void display();
};
int Base::display(){
{ std::cout << "Base::display\n"; }
}
class Derived : public Base {
public:
void display() { std::cout << "Derived::display\n"; }
};
int main() {
Derived d;
Base* ptr = &d;
ptr->display(); // 输出 "Base::display"
}
这样就可以编译通过,因为编译器在解析ptr->display()
时,可以正确找到实现方法的地址。
动态绑定
如果你将Base::display
声明为虚函数,那么ptr->display()
将会调用Derived::display
,如下所示:
class Base {
public:
virtual void display() { std::cout << "Base::display\n"; }
};
class Derived : public Base {
public:
void display() override { std::cout << "Derived::display\n"; }
};
int main() {
Derived d;
Base* ptr = &d;
ptr->display(); // 输出 "Derived::display"
}
函数声明
我们可以具有没有方法体的函数声明
void display(); 需要在外部通过::display()的方式提供实现
vitual void display(); 必须在子类中重写
pure
"declared pure"通常指的是一个纯虚函数。纯虚函数是在基类中声明但不提供实现的虚函数。子类需要为纯虚函数提供实现。如果一个类中有至少一个纯虚函数,那么这个类就是抽象类,不能被实例化(也就是说,你不能创建这个类的对象)。
你可以通过在函数声明后面添加= 0
来声明纯虚函数。例如:
class BaseClass {
public:
virtual void display() = 0; // 纯虚函数
};
在这个例子中,BaseClass
是一个抽象类,你不能创建BaseClass
的对象。如果你有一个DerivedClass
继承自BaseClass
,那么DerivedClass
需要提供display
函数的实现。
也就是说=0
这里表示没有实现,子类必须实现该方法。
不同于只使用virtual的方法,因为该方法可以有实现,子类可以不用实现,如上面的 virtual void display() { std::cout << "Base::display\n"; }
const_cast和dynamic_cast
const_cast
和dynamic_cast
是C++中的两种类型转换操作符。
const_cast
:const_cast
用于修改类型的const
属性。它允许你将一个const
类型的对象转换为非const
类型,或者将一个非const
类型的对象转换为const
类型。这在某些情况下是有用的,例如当你需要调用一个只接受非const
参数的函数,但你只有一个const
对象时。请注意,使用const_cast
去除const
属性可能导致未定义行为,如果你试图修改一个本来应该是const
的对象。
示例:
const int a = 10;
int* b = const_cast<int*>(&a); // 去除const属性
*b = 20; // 修改原本应该是const的对象可能导致未定义行为
dynamic_cast
:dynamic_cast
用于在类层次结构中执行安全的向下类型转换。(强转只支持向上转型)它主要用于将基类指针或引用转换为派生类指针或引用。如果转换失败(例如,如果基类指针实际上并不指向派生类对象),dynamic_cast
将返回空指针(对于指针类型)或抛出异常(对于引用类型)。dynamic_cast
只能用于包含虚函数的类,因为它依赖于运行时类型信息(RTTI)来执行类型检查。
示例:
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
void derivedFunction() {
std::cout << "Derived function called" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
derivedPtr->derivedFunction(); // 调用派生类的函数
} else {
std::cout << "Type conversion failed" << std::endl;
}
delete basePtr;
}
在这个例子中,dynamic_cast
将basePtr
转换为Derived*
类型。因为basePtr
实际上指向一个Derived
对象,所以转换成功,并调用derivedFunction
。如果basePtr
指向一个不是Derived
类型的对象,那么dynamic_cast
将返回空指针。
指针类型 引用类型
指针类型和引用类型在C++中都是一种间接访问数据的方式,但它们之间存在一些关键的区别:
- 初始化:引用在声明时必须被初始化,而且一旦被初始化后就不能改变;指针则可以在任何时候被初始化和改变。
- 空值:指针可以为NULL,而引用不能。这是因为引用总是需要一个实际的对象来绑定。
- 指针运算:指针支持算术运算(例如,增加或减少指针的值),而引用则不支持。
- “引用的引用”:C++不支持引用的引用(或者说,没有“引用的数组”),但可以有指针的指针,或者指针的数组。
- 间接性:指针是一个实际的变量,所以在使用指针时有一个间接的步骤。当我们有一个指针变量时,我们需要解引用它来获取它指向的值。而引用则更像是被引用的变量的一个别名,使用引用就像直接使用原始变量一样。
总结:引用类型一旦声明就不可以在重新赋值(不像java那样),指针类型一旦声明就可以重新赋值(像java那样)。
符号{}
在C++中,int x3 = {2};
和int x3 = 2;
这两种写法都可以用来定义并初始化一个整数变量。在这个特定的例子中,它们的效果是完全一样的:都会创建一个名为x3的整数变量,并将其初始化为2。
然而,使用大括号 {}
进行初始化(也被称为列表初始化或统一初始化)有一些优点:
-
它更通用。你可以用它来初始化任何类型的对象,包括数组、结构体、容器等。
-
它更安全。例如,如果你试图用一个不能无损转换的值来初始化变量,编译器会报错。比如说:
int x = {3.14}; // 错误:不能从double到int进行无损转换
而如果你使用
=
进行初始化,则编译器只会给出警告,并且仍然会进行不安全的转换:int x = 3.14; // 警告:从double到int进行了可能丢失数据的转换
因此,在C++11及以后版本中,推荐使用 {}
进行初始化。
为什么 auto x7 = (void*){0};可以编译通过,而auto x8 = (void*){1};编译报错呢?
首先,需要指出的是 (void*){0}
和 (void*){1}
这种写法并不是标准C++语言的一部分。它们类似于C语言中复合字面量(Compound Literals)的概念,但是复合字面量在C++中并没有被支持。
然而,有些编译器(例如GCC和Clang)为了兼容C99,选择接受这种语法。但这并不意味着所有的编译器都会接受,或者在所有情况下都能正确工作。
对于 (void*){0}
能够通过编译,一个可能的解释是:0
在这里被视为一个空指针常量,因此 (void*){0}
创建了一个临时的 void*
类型对象,并初始化为 nullptr
。这与直接写 nullptr
或者 (void*)0
是等价的。
然而,(void*){1}
尝试使用 1
(非零值)来初始化一个 void*
对象。这通常是错误的,因为除非你正在进行某种底层编程或硬件操作(在这种情况下你应该明确地使用强制类型转换),否则不能简单地假设 1
是一个有效的指针地址。
总之,即使 (void*){0}
在某些编译器上可以工作,我也强烈建议你避免使用这种语法。在C++中,应该使用 nullptr
来表示空指针,或者如果你需要将整数值转换为指针,应该明确地使用强制类型转换(reinterpret_cast)。
预编译
#if t
if(k){
__android_log_write(ANDROID_LOG_INFO, "日志", "前面");
}else
#endif
{
__android_log_write(ANDROID_LOG_INFO, "日志", "后面");
}
当t为真时:
if(k){
__android_log_write(ANDROID_LOG_INFO, "日志", "前面");
}else
{
__android_log_write(ANDROID_LOG_INFO, "日志", "后面");
}
当t为假时:
{
__android_log_write(ANDROID_LOG_INFO, "日志", "后面");
}
所以#if #else #endif只是简单在编译期间根据条件是否隐藏中间的代码,并没有添油加醋。所以#endif可以放在任意一行,没有限制,但是需要注意代码格式,防止隐藏后出现bug.
offsetof
C 库宏 offsetof(type, member-designator) 会生成一个类型为 size_t 的整型常量,它是一个结构成员相对于结构开头的字节偏移量。成员是由 member-designator 给定的,结构的名称是在 type 中给定的。
声明
下面是 offsetof() 宏的声明。
offsetof(type, member-designator)
参数
- type -- 这是一个 class 类型,其中,member-designator 是一个有效的成员指示器。
- member-designator -- 这是一个 class 类型的成员指示器。
返回值
该宏返回类型为 size_t 的值,表示 type 中成员的偏移量。
实例
下面的实例演示了 offsetof() 宏的用法。
#include <stddef.h>
#include <stdio.h>
struct address {
char name[50];
char street[50];
int phone;
};
int main()
{
printf("address 结构中的 name 偏移 = %d 字节。\n",
offsetof(struct address, name));
printf("address 结构中的 street 偏移 = %d 字节。\n",
offsetof(struct address, street));
printf("address 结构中的 phone 偏移 = %d 字节。\n",
offsetof(struct address, phone));
return(0);
}
让我们编译并运行上面的程序,这将产生以下结果:
address 结构中的 name 偏移 = 0 字节。
address 结构中的 street 偏移 = 50 字节。
address 结构中的 phone 偏移 = 100 字节。
来源:www.runoob.com/cprogrammin…
memcpy
C 库函数 void memcpy(void *str1, const void *str2, size_t n)
从存储区 str2 复制 n 个字节到存储区 str1。
获取当前时间
chrono::system_clock::time_point currentTime = chrono::system_clock::now();
long lastTime = chrono::system_clock::to_time_t(currentTime);//秒
long lastTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime.time_since_epoch()).count();//毫秒
typename
模板中定义成员变量类型
class M{
public:
typedef int Nest;
};
template <class T> void getTem(){
typename T::Nest a;
}
这里的typena定义了一个T::Nest型的变量a
由于typename表示跟在他后面的东西是一个不确定的类型。所以也可以这样写:
class M{
public:
typedef int Nest;
};
template <typename T> void getTem(){
typename T::Nest a;
}
这里的T不仅仅可以是class,也可以是其他类型,这就是typename的作用。
模板中定义返回类型
这是duration.h中的源码
template <class _ToDuration, class _Rep, class _Period>
inline _LIBCPP_INLINE_VISIBILITY
_LIBCPP_CONSTEXPR
typename enable_if
<
__is_duration<_ToDuration>::value,
_ToDuration
>::type
duration_cast(const duration<_Rep, _Period>& __fd)
{
return __duration_cast<duration<_Rep, _Period>, _ToDuration>()(__fd);
}
上面通过template <class _ToDuration, class _Rep, class _Period>定义了三个类类型的泛型,
而这个函数的返回类型就是enable_if
enable_if< __is_duration<_ToDuration>::value, _ToDuration >::type
因为enable_if用到了泛型,所以需要有typename关键字。如果enable_if这个结构体没有引用泛型则不需要此关键字。例如:
class M{
public:
typedef int Nest;
};
template <typename T> class K{
public:
struct B
{
/* data */
};
};
template <class T> typename K<T>::B getTem(){
typename T::Nest a;
}
或者
template <class T> K<M>::B getTem(){
typename T::Nest a;
}
template还能这样用
template表示后面的<>是模板
template <int T,typename L> class M{
};
template <typename T> class M<2,T>{
public:
typedef int Nest;
struct B
{
/* data */
};
};
template <class T> typename M<2,T>::B getTem(){
typename T::Nest a;
}
int main(int argc, char const *argv[])
{
auto pp = 0;
getTem<M<2,int>>();
return 0;
}
当使用M<2,int>时,该类型就是上面的M<2,T>,当为其他时则是上面的M类型,这里你可以看到有两个同名的类。
同int指定具体的值一样,int可以换成class,根据传入不同的类名而使用不同M类。例如:
class K{
};
template <class T,typename L> class M{
};
template <typename T> class M<K,T>{
public:
typedef int Nest;
struct B
{
/* data */
};
};
这叫做模板类的特化。
K * const *和K **const
对象a = 对象b
在 C++ 中,当你执行 a = b
时,这表示将对象 b 的值赋给对象 a。这会调用对象 a 的赋值运算符重载函数(如果已经定义了的话),否则会使用默认的浅层复制(即将 b 对象的所有成员变量的值复制给 a 对象的对应成员变量)。
另一方面,&a = &b
这样的语句是不合法的。&
操作符用于取地址,因此 &a
表示对象 a 的地址,而 &b
表示对象 b 的地址。这两者的地址是不可更改的,因此你不能将一个对象的地址赋给另一个对象的地址。
总结一下:
a = b
表示将对象 b 的值赋给对象 a。&a
表示对象 a 的地址,&b
表示对象 b 的地址。&a = &b
这样的赋值是非法的,因为对象的地址是不可更改的。因为&a
的结果并不是一个类型,而是一个值像100
这样,而值是不可以更改的,变量可以。
如果你希望实现自定义的赋值行为,你可以通过重载赋值运算符 operator=
来实现。这样你可以定义对象赋值时的具体行为,而不仅仅是简单的成员变量复制。
与java的不同点:
C++ 中的等号(赋值操作符)和 Java 中的等号在处理对象时表现不同,这主要是因为 C++ 和 Java 在语义上对待对象和赋值的方式不同。
在 C++ 中:
- 对于内置类型(如
int
、float
等)和结构体(如AVRational
),赋值操作符执行的是浅拷贝(shallow copy),即直接复制值或对象的每个成员到另一个对象。 - 对于类对象,赋值操作符可以被重载。默认情况下,如果没有提供自定义的赋值操作符,编译器会生成一个默认的赋值操作符,它执行的也是成员到成员的浅拷贝。如果需要深拷贝(deep copy)或特殊的赋值行为,开发者需要显式地提供一个赋值操作符重载。
在 Java 中:
- 所有的对象赋值都是引用赋值。当你将一个对象赋值给另一个对象时,你只是拷贝了对象的引用,而不是对象本身。这意味着两个引用指向了同一个对象,因此通过任何一个引用所做的更改都会反映到另一个引用上。
- 对于基本数据类型(如
int
、double
等),Java 的赋值行为与 C++ 类似,即直接拷贝值。
因此,在 C++ 中,对于非类对象的赋值操作(比如 AVRational
这样的结构体),a = b;
之后,a
和 b
是完全独立的副本。而在 Java 中,如果 a
和 b
是对象的引用,a = b;
之后,a
和 b
指向同一个对象,对对象的任何更改都会通过两个引用都能看到。
std::variant
std::variant
是 C++17 标准库中引入的一个新特性。它是一个类型安全的联合体,可以在一定范围的类型中存储并访问值,而无需预先确定该值的类型。需要引入头文件:#include <variant>
std::variant
是泛型编程的一个重要工具,它可以在同一时间只存储一个值,这个值可以是定义 std::variant
时列出的类型中的任何一个。你可以像使用普通类型一样使用 std::variant
,而不用担心类型不匹配或者类型转换的问题。
以下是一个 std::variant
的使用示例:
#include <variant>
#include <iostream>
int main() {
std::variant<int, float, std::string> v;
v = 10;
std::cout << std::get<int>(v) << std::endl; // 输出:10
v = 220.5f;
std::cout << std::get<float>(v) << std::endl; // 输出:220.5
v = "C++ Programming";
std::cout << std::get<std::string>(v) << std::endl; // 输出:C++ Programming
return 0;
}
在这个例子中,我们定义了一个可以存储 int
、float
或 std::string
的 std::variant
。然后我们可以将这些类型的值赋给 v
,并使用 std::get
函数来访问它。
请注意,如果你尝试使用 std::get
访问当前未存储在 std::variant
中的类型,它将抛出一个 std::bad_variant_access
异常。为了避免这种情况,你可以使用 std::holds_alternative
函数来检查 std::variant
当前是否存储了特定的类型。
if (std::holds_alternative<int>(v)) {
std::cout << std::get<int>(v) << std::endl;
}
总的来说,std::variant
提供了一种类型安全的方式来在同一时间处理多种可能的类型,这使得它在许多情况下都比使用 union
更为便利和安全。
但与 std::any
不同的是,std::variant
在编译时就确定了可能的类型,因此在使用时更加类型安全。
std::any
std::any
是 C++17 标准库中引入的另一个新特性。它是一个动态类型的容器,可以存储几乎任何类型的值。与 std::variant
不同,std::any
不需要在编译时知道可能的类型,它可以在运行时接受任何类型。需要引入头文件:#include <any>
以下是一个 std::any
的使用示例:
#include <any>
#include <iostream>
#include <string>
int main() {
std::any a;
a = 10;
std::cout << std::any_cast<int>(a) << std::endl; // 输出:10
a = 220.5;
std::cout << std::any_cast<double>(a) << std::endl; // 输出:220.5
a = std::string("C++ Programming");
std::cout << std::any_cast<std::string>(a) << std::endl; // 输出:C++ Programming
return 0;
}
在这个例子中,我们定义了一个 std::any
变量 a
,然后我们可以将任何类型的值赋给 a
,并使用 std::any_cast
函数来访问它。
请注意,如果你尝试使用 std::any_cast
访问当前未存储在 std::any
中的类型,它将抛出一个 std::bad_any_cast
异常。为了避免这种情况,你可以使用 std::any::type
函数来检查 std::any
当前存储的类型。
if (a.type() == typeid(int)) {
std::cout << std::any_cast<int>(a) << std::endl;
}
总的来说,std::any
提供了一种动态类型的方式来在同一时间处理多种可能的类型,这使得它在许多情况下都比使用 union
或 std::variant
更为灵活。然而,这种灵活性也带来了一些额外的复杂性,因为你需要在运行时检查和处理类型。
union
union
是一种特殊的数据结构,允许在相同的内存位置存储不同的数据类型。union
的所有成员共享同一块内存,这样可以更灵活地使用相同的内存空间存储不同类型的数据。
union
的定义形式如下:
union UnionName {
// 成员列表
type1 member1;
type2 member2;
// ...
};
其中,UnionName
是 union 的名称,而 member1
、member2
等是 union 的成员。这些成员可以是不同的数据类型,但它们共享相同的内存空间。union
的大小取决于它最大的成员的大小。
下面是一个简单的示例,说明了如何使用 union
:
#include <iostream>
union MyUnion {
int intValue;
double doubleValue;
char charValue;
};
int main() {
MyUnion myUnion;
myUnion.intValue = 42;
std::cout << "Int value: " << myUnion.intValue << std::endl;
myUnion.doubleValue = 3.14;
std::cout << "Double value: " << myUnion.doubleValue << std::endl;
myUnion.charValue = 'A';
std::cout << "Char value: " << myUnion.charValue << std::endl;
return 0;
}
在上面的例子中,MyUnion
包含了一个整数、一个双精度浮点数和一个字符。不同的成员可以在不同的时刻被赋值和访问,但同时只能有一个成员被有效使用。这是因为所有成员共享同一块内存,改变其中一个成员会影响其他成员。使用 union
需要小心,确保在任何时刻只使用了正确的成员。
strtol
strtol
是C和C++编程语言中的一个函数,用于将字符串转换为长整数(long)。函数原型如下:
long int strtol (const char* str, char** endptr, int base);
参数说明:
str
:这是指向要转换的字符串的指针。endptr
:这是一个字符指针的指针,函数会将其设置为在str
中第一个无法被转换的字符的位置。如果不关心这个值,可以将该参数设置为NULL
。base
:这是转换的基数,它必须在2和36之间,或者是特殊值0。如果base
是0,那么函数会根据str
的前缀来确定实际的基数:如果str
以"0x"或"0X"开始,那么基数是16;如果str
以"0"开始,基数是8;否则基数是10。
strtol
函数会从str
指向的字符串开始,跳过前面的空白字符,然后尝试将后面的字符转换为base
指定基数的长整数,转换会在遇到无法转换的字符时停止。
如果转换成功,函数会返回转换得到的长整数。如果转换失败,函数会返回0,并将全局变量errno
设置为ERANGE
。
std::any判断是否属于同一类型
在C++中,你可以使用std::any
结合std::type_index
来实现类似Kotlin中的when
语句的功能。以下是一个示例代码:
#include <iostream>
#include <any>
#include <typeindex>
#include <typeinfo>
void processAny(const std::any& value) {
if (value.type() == typeid(int)) {
std::cout << "It's an int: " << std::any_cast<int>(value) << std::endl;
} else if (value.type() == typeid(float)) {
std::cout << "It's a float: " << std::any_cast<float>(value) << std::endl;
} else {
std::cout << "It's neither int nor float" << std::endl;
}
}
int main() {
std::any value1 = 10;
std::any value2 = 3.14f;
processAny(value1);
processAny(value2);
return 0;
}
在这个示例中,processAny
函数接受一个 std::any
类型的参数,并使用 type()
方法来获取存储的值的类型信息,然后使用 any_cast
来根据类型执行相应的操作。这样你就可以根据存储在 std::any
中的具体类型来执行不同的逻辑,类似于 Kotlin 中的 when
语句。
需要注意的是,对于任何不同的类型,你都需要手动处理。如果你有更多的类型需要处理,你可能需要使用更复杂的分发机制,比如使用函数指针数组或者std::variant
来处理更多的情况。
生产者-消费者模式
pthread_cond_wait
是一个线程同步函数,用于阻塞当前线程,直到另一个线程通过pthread_cond_signal
或pthread_cond_broadcast
来唤醒它。
这个函数通常用于实现生产者-消费者模式。比如说,你有一个生产者线程向队列中添加元素,和一个消费者线程从队列中移除元素。如果队列为空,消费者线程就没有工作可做,所以它会调用pthread_cond_wait
来等待生产者线程添加一个元素。当生产者线程添加了一个元素,它就会调用pthread_cond_signal
来唤醒消费者线程。
pthread_cond_wait
函数的原型如下:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这个函数接受两个参数:一个条件变量(cond
)和一个互斥量(mutex
)。在调用pthread_cond_wait
之前,你需要先锁定互斥量,pthread_mutex_lock。
如果pthread_cond_wait
成功,它返回0。如果失败,它返回一个错误码。
*(const obj **)
这个的作用是获取该对象的第一个属性,比如:
struct OBJ{
void *first;
};
OBJ * obj;
*(const OBJ **)obj 就是first;
void * p;
*(const obj **)obj = p;就是obj->first = p;
也就是不会影响obj指针的值,就是原指向的对象不变,而是修改指向对象的第一个属性的值。最前面那个*
一定要带着
enum
enum Status {
OK = -1,
ERROR = 1,
PENDING
};
PENDING值会是多少?
枚举成员 PENDING 没有显式地指定一个整数值,因此它将会遵循枚举成员的递增规律。根据这个规律,PENDING 的值将会是 ERROR 的值加上 1。在这种情况下,ERROR 的值是 1,因此 PENDING 的值将会是 2。
因此,根据你的示例代码,枚举类型 Status 的成员将分别具有以下值:
- OK 的值是 -1
- ERROR 的值是 1
- PENDING 的值是 2
在so库刚加载到内存或者从内存离开时被系统自动调用的函数
.init_array
段是 ELF (Executable and Linkable Format) 文件格式中的一部分,它用于存储指向初始化函数的指针数组。这些初始化函数在动态库被加载到进程空间时(在 main()
函数执行之前)由动态链接器调用。相似地,还有一个 .fini_array
段,它包含的函数会在库卸载或程序结束时被调用。
在 C 或 C++ 程序中,.init_array
段通常用于存放构造函数(C++中的全局对象构造函数)和其他初始化代码。如果你想要往 .init_array
段添加函数,你可以使用 GCC 的构造函数属性:
__attribute__((constructor))
void my_init_function() {
// ... 初始化代码 ...
}
使用 __attribute__((constructor))
属性声明的函数会在程序启动时自动执行,而不需要手动调用。这些函数的指针会被放置在 .init_array
段中。类似地,如果你想在程序退出时执行代码,可以使用 __attribute__((destructor))
属性:
__attribute__((destructor))
void my_fini_function() {
// ... 清理代码 ...
}
这将确保 my_fini_function
函数在程序结束时被调用,其指针会被放在 .fini_array
段中。
在链接阶段,链接器会收集所有带有这些属性的函数,并将它们的地址放入对应的 .init_array
或 .fini_array
段中。这样,当程序或库被加载时,动态链接器就会按照 .init_array
中的顺序调用这些函数。
要注意的是,这些属性是 GCC 特有的,并且可能不会在所有编译器上工作。另外,过度依赖构造函数和析构函数可能会使程序的初始化和终止过程变得复杂和难以调试,因此应当谨慎使用。
c语言将函数类型声明写在括号外
在Linux的源码中会看到这种旧的写法。
这种写法,void p(a) char *a; { }
,是C语言中的旧式(K&R)函数定义语法。这种语法源于最初的C语言版本,由Kernighan和Ritchie在他们的经典书籍《The C Programming Language》中介绍。在这种风格中,函数参数的类型是在函数声明的后面分开声明的。
相比之下,void p(char *a) { }
使用的是标准的ANSI C语法,这种语法在1989年的ANSI C标准化后成为了标准做法。在这种风格中,参数类型直接在参数名之前声明,这使得代码更加清晰易读。
两种语法都是有效的,但是现代C语言编程中更推荐使用ANSI C语法,因为它更加标准化和易于理解。旧式K&R语法在一些旧的代码或者是需要与旧系统兼容的情况下仍可能会见到。
文件操作
打开文件 fopen
FILE* _Nullable fopen(const char* _Nonnull __path, const char* _Nonnull __mode);
filename
参数是一个字符串,表示要打开的文件的路径和名称。mode
参数是一个字符串,指定文件的打开模式,例如 "r" 表示只读,"w" 表示写入(如果文件不存在则创建,如果存在则截断为空文件),"a" 表示追加等等。
mode
参数用于指定文件打开的模式,它是一个字符串,包含了表示文件操作权限和方式的字符。
一些常见的 mode
参数包括:
"r"
: 以只读方式打开文件。文件必须存在,否则打开失败。"w"
: 以写入方式打开文件。如果文件存在,则文件内容被截断为空;如果文件不存在,则创建一个新文件。"a"
: 以追加方式打开文件。如果文件存在,在文件末尾追加写入;如果文件不存在,则创建一个新文件。"r+"
: 以读写方式打开文件。文件必须存在。"w+"
: 以读写方式打开文件。如果文件存在,则文件内容被截断为空;如果文件不存在,则创建一个新文件。"a+"
: 以读写方式打开文件,并在文件末尾追加写入。如果文件不存在,则创建一个新文件。
在指定模式时,可以附加额外的字符来表示文件类型或者以二进制模式打开文件。例如:
"rb"
: 以二进制格式打开文件以供读取。"wb"
: 以二进制格式打开文件以供写入。"ab"
: 以二进制格式追加写入文件。"r+b"
或"rb+"
: 以二进制格式以读写方式打开文件。"w+b"
或"wb+"
: 以二进制格式以读写方式打开文件(如果文件存在则截断为空,不存在则创建新文件)。"a+b"
或"ab+"
: 以二进制格式以读写方式打开文件并在文件末尾追加写入。
读取内容
fgets
fgets
函数:
-
语法:
char *fgets(char *str, int n, FILE *stream);
-
功能:从指定的文件流(通常是通过
fopen
打开的文件)读取一行内容。 -
参数:
str
: 用于存储读取内容的字符数组。n
: 最大读取字符数,包括字符串结束符\0
fgetln
函数:
-
语法:
char *fgetln(FILE *stream, size_t *len);
-
功能:从指定的文件流读取一行,并返回该行的首地址。同时,
len
参数会被设置为读取的字符数。 -
示例:
``` FILE *filePointer; filePointer = fopen("example.txt", "r"); size_t length; char *line = fgetln(filePointer, &length); if (line != NULL) { // 处理读取的行 } ```
fgetwc
和 fgetws
函数:
-
fgetwc
读取一个宽字符,而fgetws
读取一个宽字符字符串。 -
语法:
wint_t fgetwc(FILE *stream);
和wchar_t *fgetws(wchar_t *ws, int n, FILE *stream);
-
示例:
``` FILE *filePointer; filePointer = fopen("example.txt", "r"); wint_t wc = fgetwc(filePointer); if (wc != WEOF) { // 处理读取的宽字符 } ```
fgetc
函数:
- 语法:`int fgetc(FILE *stream);`
- 功能:从指定的文件流中读取一个字符。
分配内存
在 C 语言中,有几个用于在堆区(heap)分配内存的函数。这些函数包括:
-
malloc
函数:- 语法:
void *malloc(size_t size);
- 功能:分配指定大小的内存块,并返回一个指向该内存块起始地址的指针。注意,
malloc
不会初始化内存中的值,它分配的内存中的内容是未定义的。
int *arr = (int *)malloc(5 * sizeof(int));
- 语法:
-
calloc
函数:- 语法:
void *calloc(size_t num, size_t size);
- 功能:分配指定数量和大小的连续内存块,并返回一个指向该内存块起始地址的指针。与
malloc
不同,calloc
会将内存中的每个字节初始化为零。
int *arr = (int *)calloc(5, sizeof(int));
- 语法:
-
realloc
函数:- 语法:
void *realloc(void *ptr, size_t size);
- 功能:重新分配之前由
malloc
或calloc
分配的内存块的大小。它可以用于扩展或缩小内存块。如果需要扩展,可能会改变内存块的地址。
int *arr = (int *)malloc(5 * sizeof(int)); arr = (int *)realloc(arr, 10 * sizeof(int));
- 语法:
-
free
函数:- 语法:
void free(void *ptr);
- 功能:释放之前由
malloc
、calloc
或realloc
分配的内存块。释放后,指针ptr
不再指向有效的内存地址。
int *arr = (int *)malloc(5 * sizeof(int)); free(arr);
- 语法:
这些函数提供了在运行时动态分配和管理内存的能力。使用它们时需要小心,确保在不再需要内存时释放它,以防止内存泄漏。
字符串
strspn
strspn
函数通常用于计算字符串中连续包含在指定字符集合中的字符数。该函数返回字符串开头连续包含指定字符集合中字符的长度。这在C语言中很有用,特别是在需要验证字符串是否符合特定格式时。例如,你可以使用 strspn
函数来检查字符串是否只包含数字字符或字母字符。
str = "[];;dfasfda,d"
strspn(str,"=,;[")
结果为4,后面的逗号不算,因为没有在开头
strtol
strtol
是C和C++编程语言中的一个函数,用于将字符串转换为长整数(long)。函数原型如下:
long int strtol (const char* str, char** endptr, int base);
参数说明:
str
:这是指向要转换的字符串的指针。endptr
:这是一个字符指针的指针,函数会将其设置为在str
中第一个无法被转换的字符的位置。如果不关心这个值,可以将该参数设置为NULL
。base
:这是转换的基数,它必须在2和36之间,或者是特殊值0。如果base
是0,那么函数会根据str
的前缀来确定实际的基数:如果str
以"0x"或"0X"开始,那么基数是16;如果str
以"0"开始,基数是8;否则基数是10。
strtol
函数会从str
指向的字符串开始,跳过前面的空白字符,然后尝试将后面的字符转换为base
指定基数的长整数,转换会在遇到无法转换的字符时停止。
如果转换成功,函数会返回转换得到的长整数。如果转换失败,函数会返回0,并将全局变量errno
设置为ERANGE
。
其他
-
strlen:返回字符串的长度。
char str[] = "Hello"; int len = strlen(str); // len 的值为 5
-
strcpy:将一个字符串复制到另一个字符串。
char source[] = "Hello"; char destination[20]; strcpy(destination, source); // destination 的值为 "Hello"
-
strcat:将一个字符串连接到另一个字符串的末尾。
char str1[] = "Hello"; char str2[] = " World"; strcat(str1, str2); // str1 的值为 "Hello World"
-
strcmp:比较两个字符串。
char str1[] = "apple"; char str2[] = "banana"; int result = strcmp(str1, str2); // result 的值为负数,表示 str1 小于 str2
-
strchr:在字符串中查找特定字符的第一次出现。
char str[] = "Hello, world"; char *ptr = strchr(str, 'o'); // ptr 的值为 "o, world"
-
strstr:在字符串中查找特定子字符串的第一次出现。
char str[] = "Hello, world"; char *ptr = strstr(str, "world"); // ptr 的值为 "world"
-
sscanf
函数是 C 语言标准库中的一个函数,用于按照指定的格式从一个字符串中读取数据。它的原型如下:int sscanf(const char *str, const char *format, ...);
str
:要解析的字符串。format
:包含格式说明符的字符串,指定了如何解析输入字符串。...
:可变参数列表,用于接收解析后的数据。
sscanf
的工作方式类似于scanf
,但不是从标准输入读取,而是从一个字符串中读取。以下是一个简单的例子,演示了
sscanf
的基本用法:#include <stdio.h> int main() { char input_string[] = "John 25 175.5"; char name[20]; int age; float height; // 使用 sscanf 解析字符串 int result = sscanf(input_string, "%s %d %f", name, &age, &height); if (result == 3) { // 解析成功 printf("Name: %s\n", name); printf("Age: %d\n", age); printf("Height: %.2f\n", height); } else { // 解析失败 printf("Failed to parse input string.\n"); } return 0; }
sscanf
不是全部匹配才赋值,他是依次赋值,比如:
对于数据:720a214000-720a245000 r-xp 00014000
sscanf(buf, "%" PRIxPTR "-%*lx %*4s 00000000", &base_addr);
仍然能正确解析。
因为他是依次解释,当遇到00000000发现不匹配才停止,而00000000前面的会正常解析。
格式化参数
在 C++ 中,格式化输出时使用的参数主要有以下几种:
-
整数格式化输出:
%d
:以十进制形式输出整数。%x
:以十六进制形式输出整数,小写字母。%X
:以十六进制形式输出整数,大写字母。%o
:以八进制形式输出整数。%u
:以无符号十进制形式输出整数。
-
浮点数格式化输出:
%f
:以小数形式输出浮点数。%e
:以指数形式输出浮点数,小写字母。%E
:以指数形式输出浮点数,大写字母。%g
:根据值的大小选择%e
或%f
形式输出。%a
:以十六进制浮点数的形式输出浮点数。
-
字符和字符串格式化输出:
%c
:输出一个字符。%s
:输出字符串。
-
指针格式化输出:
%p
:输出指针地址。
-
长整型格式化输出:
%ld
:以长整型形式输出整数。%lx
:以长整型十六进制形式输出整数。
-
长长整型格式化输出:
%lld
:以长长整型形式输出整数。%llx
:以长长整型十六进制形式输出整数。
内存操作
-
memcpy
: 复制内存区域。- 用法:
void* memcpy(void* dest, const void* src, size_t n);
- 示例:
memcpy(dest, src, 100);
从src
地址开始复制 100 个字节到dest
地址。
- 用法:
-
memmove
: 复制内存区域,适用于源和目标内存区域重叠的情况。- 用法:
void* memmove(void* dest, const void* src, size_t n);
- 示例:
memmove(dest, src, 100);
从src
地址开始复制 100 个字节到dest
地址,即使这两个内存区域重叠。
- 用法:
-
memset
: 设置内存区域的字节。- 用法:
void* memset(void* ptr, int value, size_t num);
- 示例:
memset(arr, 0, 10 * sizeof(int));
将arr
指向的内存区域的前 10 个整数的内存设置为 0。
- 用法:
调用 memset(arr, 10000, 1)
函数时,其实有一些需要注意的地方。
-
参数解释:
arr
是一个指针,指向要设置的内存的起始地址。- 第二个参数在这里是
10000
,但这里存在一个问题。memset
函数的第二个参数应该是一个int
类型,但它实际上是以一个无符号字符(unsigned char
)的形式处理的。这意味着即使你传递了10000
,它实际上会被截断为一个字节(8 位)。 - 第三个参数
1
表示操作的字节数。在这里,它指明只设置arr
指向的内存的第一个字节。
-
值的截断:
10000
在二进制中表示为0010011100100000
(16位)。然而,memset
会截断这个值到一个字节(8位),所以只会考虑最低的8位。在这个例子中,10000
的最低8位是10000000
(十进制中的128)。- 因此,实际上这个函数调用将
arr
指向的内存的第一个字节设置为128
,而不是10000
。
简而言之,memset(arr, 10000, 1)
实际上会将 arr
指向的内存的第一个字节设置为 128
。这是因为 10000
在传递给 memset
时会被截断为一个字节。这种行为可能是非预期的,因此在使用 memset
时需要特别注意值的范围和类型。
读个字符串 写在一起会自动连接在一起
auto p = "cxxcv" "xca";//相当于auto p = "cxxcvxca";
实践:
#define PRIxPTR "l""x"
sscanf(buf, "%" PRIxPTR "-%*lx %*4s 00000000", &base_addr);
就相当于:
#define PRIxPTR "l""x"//相当于#define PRIxPTR "lx"
sscanf(buf, "%" PRIxPTR "-%*lx %*4s 00000000", &base_addr);//相当于sscanf(buf, "%lx-%*lx %*4s 00000000", &base_addr);
这里的*
表示忽略该值
list
使用 STL(Standard Template Library)的各种功能时,你需要引入相应的头文件。下面是一些常见的 STL 组件及其对应的头文件:
-
容器(Containers) :
#include <vector>
:向量(std::vector
)#include <list>
:链表(std::list
)#include <deque>
:双端队列(std::deque
)#include <queue>
:队列(std::queue
)#include <stack>
:栈(std::stack
)#include <set>
:集合(std::set
)#include <map>
:映射(std::map
)#include <unordered_set>
:无序集合(std::unordered_set
)#include <unordered_map>
:无序映射(std::unordered_map
)#include <array>
:数组(std::array
)
std::vector
是 C++ STL 中的一种动态数组,它可以在运行时改变大小。以下是一些基本的 std::vector
使用方法:
首先,你需要包含 <vector>
头文件,然后你可以声明一个 std::vector
对象。
#include <vector>
std::vector<int> my_vector;
你可以使用 push_back()
方法在向量的末尾添加元素:
my_vector.push_back(1);
my_vector.push_back(2);
my_vector.push_back(3);
你可以使用索引来访问 std::vector
中的元素,就像使用数组一样:
int first_element = my_vector[0]; // first_element 现在是 1
你也可以使用 at()
方法通过索引访问元素,这个方法在索引超出范围时会抛出异常:
int second_element = my_vector.at(1); // second_element 现在是 2
std::vector
的 size()
方法可以返回向量中的元素数量:
size_t size = my_vector.size(); // size 现在是 3
你可以使用 begin()
和 end()
方法获取到指向 std::vector
开始和结束的迭代器,这在使用某些 STL 算法时非常有用:
std::vector<int>::iterator begin = my_vector.begin();
std::vector<int>::iterator end = my_vector.end();
std::vector
还有许多其他的方法,例如 insert()
, erase()
, pop_back()
, resize()
, clear()
等等,它们提供了更多的操作向量的方式。
std::bind
std::bind
是 C++ 标准库中的一个功能,它可以将可调用对象(如函数、函数指针、函数对象或 lambda 表达式)与其参数一起绑定,创建出一个新的可调用对象。这对于延迟函数调用或者调整函数参数非常有用。
下面是一个使用 std::bind
的基本例子:
#include <iostream>
#include <functional>
void print(int i) {
std::cout << i << '\n';
}
int main() {
auto bound_print = std::bind(print, 5);
bound_print(); // 输出:5
}
在这个例子中,std::bind
将函数 print
和参数 5
绑定在一起,创建了一个新的无参数函数 bound_print
。当调用 bound_print
时,它会调用 print
函数并传入参数 5
。
std::bind
还有一些更高级的用法。例如,你可以使用占位符(std::placeholders
)来创建一个只绑定部分参数的函数:
#include <iostream>
#include <functional>
void print(int i, int j) {
std::cout << i << " " << j << '\n';
}
int main() {
auto bound_print = std::bind(print, std::placeholders::_1, 5);
bound_print(3); // 输出:3 5
}
在这个例子中,std::bind
创建了一个新的函数 bound_print
,它接受一个参数并将其作为 print
函数的第一个参数,同时将 5
作为 print
函数的第二个参数。
需要注意的是,自从 C++11 引入了 lambda 表达式后,std::bind
的很多用法都可以用更简洁、更易读的 lambda 表达式来替代。在实际编程中,你可能会发现 lambda 表达式更加方便和灵活。
右值引用和std::move
lambda 表达式
Lambda 表达式是 C++11 和更高版本中的一个特性,它允许你定义一个匿名(即无名字的)函数,并可以在代码的任何地方使用。Lambda 表达式在很多情况下都非常有用,特别是在需要传递一个小函数作为参数的情况下。
Lambda 表达式的基本语法如下:
[捕获列表](参数列表) -> 返回类型 {
// 函数体
}
其中:
- 捕获列表:定义了 lambda 表达式可以访问的外部变量。你可以使用
=
来捕获所有外部变量的副本,或者使用&
来捕获所有外部变量的引用。你也可以混合使用=
和&
来捕获特定的变量。 - 参数列表:和普通函数的参数列表一样,定义了 lambda 表达式接收的参数。
- 返回类型:可选的,定义了 lambda 表达式的返回类型。如果省略了返回类型,编译器会根据函数体中的
return
语句自动推断返回类型。 - 函数体:定义了 lambda 表达式的行为。
下面是一个简单的 lambda 表达式的例子:
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int value = 3;
auto it = std::find_if(numbers.begin(), numbers.end(), [value](int number) {
return number > value;
});
if (it != numbers.end()) {
std::cout << "Found a number greater than " << value << ": " << *it << std::endl;
} else {
std::cout << "No numbers greater than " << value << " found." << std::endl;
}
return 0;
}
在这个例子中,我们使用了一个 lambda 表达式作为 std::find_if
函数的参数。这个 lambda 表达式接收一个 int
参数,返回一个 bool
值,表示这个数是否大于 value
。注意我们在捕获列表中使用了 value
,这样 lambda 表达式就可以访问这个外部变量。
std::initializer_list
std::initializer_list
是 C++11 引入的一种类型,用于表示一组相同类型的值。std::initializer_list
是一个轻量级的容器类型,它的主要用途是在初始化列表(也就是 {}
)中接收一组值。
例如,你可以使用 std::initializer_list
在构造函数中接收任意数量的参数:
#include <initializer_list>
#include <vector>
class MyClass {
public:
MyClass(std::initializer_list<int> init) {
for (int value : init) {
// 处理 value
}
}
};
// 使用
MyClass obj{1, 2, 3, 4, 5};
在这个例子中,MyClass
的构造函数接受一个 std::initializer_list<int>
参数。当我们用 {1, 2, 3, 4, 5}
来初始化 MyClass
对象时,这个列表中的值就被封装在 std::initializer_list<int>
对象中,然后传递给构造函数。
注意,std::initializer_list
对象中的元素是常量,不能被修改。此外,std::initializer_list
对象本身并不拥有它所包含的元素,它只是持有元素的引用。这意味着,如果 std::initializer_list
对象的生命周期结束,它所包含的元素可能就无法访问了。因此,如果你需要在 std::initializer_list
对象生命周期结束后仍然保持对元素的访问,你应该将元素复制到另一个容器中。
注意当创对象时:在大多数情况下,MyClass deri{1, 2};
和 MyClass deri(1, 2);
是等价的,它们都会调用 MyClass
类的构造函数,传入参数 1
和 2
。然而,它们之间还是存在一些微妙的差别。
- 窄化转换:使用
{}
初始化列表的方式会阻止窄化转换。窄化转换是指一些可能导致数据丢失或者改变的类型转换,例如从double
到int
,或者从int
到char
。如果你试图用一个可能导致窄化转换的值去初始化一个变量,编译器会给出警告或错误。例如,MyClass deri{1.0, 2.0};
可能会编译失败,而MyClass deri(1.0, 2.0);
则可能会成功,但可能导致数据丢失。 - 最优匹配:如果
MyClass
类有一个接受std::initializer_list<int>
的构造函数,那么MyClass deri{1, 2};
会优先调用这个构造函数,而MyClass deri(1, 2);
则会调用接受两个int
参数的构造函数。
所以,尽管大多数情况下这两种方式都可以用来初始化对象,但在某些特定的情况下,它们的行为可能会有所不同。
可以使用{}初始化的class还有哪些
C++ 提供了一些特殊的类,可以使用 {}
进行初始化,这是由编译器直接支持的。这些类包括:
-
标准库容器类:例如
std::vector
、std::array
、std::list
、std::set
、std::map
等。这些类都可以使用{}
初始化。std::vector<int> v = {1, 2, 3, 4, 5}; std::array<int, 5> a = {1, 2, 3, 4, 5}; std::set<int> s = {5, 4, 3, 2, 1}; std::map<std::string, int> m = {{"apple", 1}, {"banana", 2}};
std::pair
C++17 引入的一种新特性,叫做结构化绑定(Structured Binding)。这种特性允许你一次性声明并初始化多个变量,这些变量可以接收一个对象的各个成员。在你的例子中,std::make_pair(a, b)
返回一个 std::pair
对象,然后 c
和 d
分别接收这个对象的 first
和 second
成员。
以下是一个例子:
int a = 1;
int b = 2;
auto [c, d] = std::make_pair(a, b); // c becomes 1, d becomes 2
在这个例子中,c
和 d
被初始化为 std::pair
对象的 first
和 second
成员的值,即 1
和 2
。这种特性在处理包含多个成员的对象时非常有用,比如 std::pair
、std::tuple
或者用户定义的结构体。
define
#define ElfW(type) Elf64_ ## type
ElfW(type)
是一个宏,它将被替换为 Elf64_ ## type
。
这里的 ##
是预处理器的连接操作符,它将两个记号连接成一个记号。所以,如果你在代码中写 ElfW(int)
, 预处理器会将它替换为 Elf64_int
。
如果只写Elf64_type
而不写 ##(注意前面有空格),不会把参数type解析出来,而是看作字符
using
两者都是给类型设置一个别名,区别是using可以给模板类设置别名,而typedef则不能模板类设置设置别名。其余情况没有什么区别。
#include <vector>
using namespace std;
template<typename T>
using myvector=vector<T>;
int main(){
myvector<int> iv;
return 0;
}
编译通过。
#include <vector>
using namespace std;
template<typename T>
typedef vector<T> myvector;
int main(){
myvector<int> iv;
return 0;
}
编译报错error: template declaration of ‘typedef’
。
而已经推演过的模板类是可以用typedef设置别名的
#include <vector>
using namespace std;
typedef vector<int> intvector;
int main(){
intvector iv;
return 0;
}
return的编译器优化
-
移动构造函数:当我们创建一个新对象,并以一个将要销毁的对象(右值)初始化它时,移动构造函数会被调用。例如,使用
std::move
可以将一个左值转换为右值:MyClass obj1; MyClass obj2 = std::move(obj1); // 调用移动构造函数
或者当我们返回一个局部对象时,也会调用移动构造函数:
MyClass foo() { MyClass obj; return obj; // 调用移动构造函数 }
这里需要注意的是,C++11 及以后的版本中,编译器通常会优化掉这种情况的移动构造函数调用,直接在返回值的位置构造对象,这种技术被称为返回值优化(RVO)。
lseek获取文件长度
lseek
是一个系统调用函数,用于在文件中移动文件偏移量(文件指针)。它的原型如下:
off_t lseek(int fd, off_t offset, int whence);
-
fd
:文件描述符,表示要进行操作的文件。 -
offset
:偏移量,表示要移动的字节数。正值表示向文件末尾方向移动,负值表示向文件开头方向移动。 -
whence
:指定偏移量的基准位置。有以下几个选项:SEEK_SET
:从文件开头开始计算偏移量。SEEK_CUR
:从当前文件指针位置开始计算偏移量。SEEK_END
:从文件末尾开始计算偏移量。
lseek
函数返回新的文件偏移量,如果出现错误,返回 -1,并设置 errno
。
lseek
函数常用于文件的随机访问,可以将文件指针移动到文件的任意位置,以便读取或写入文件的特定部分。例如,可以使用 lseek
将文件指针移动到文件的开头、末尾或指定位置,然后使用 read
或 write
函数进行读取或写入操作。
获取文件长度:file_length = lseek(fd, 0, SEEK_END);
mmap内存映射
在使用 mmap
函数将文件映射到内存后,你可以通过访问映射的内存区域来获取文件的数据。映射的内存区域会被映射到进程的虚拟地址空间中,因此你可以像访问普通的内存一样来访问映射的数据。
通常,你可以使用指针来访问映射的内存数据。以下是一个简单的示例代码,展示了如何在 C 语言中使用 mmap
映射文件后获取内存数据:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
const char *file_path = "example.txt";
int fd = open(file_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
off_t file_size = lseek(fd, 0, SEEK_END);
void *mapped_data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped_data == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
// 通过指针访问映射的数据
char *data_ptr = (char *) mapped_data;
printf("Mapped data: %s\n", data_ptr);
// 访问完毕后,记得释放映射的内存
if (munmap(mapped_data, file_size) == -1) {
perror("munmap");
close(fd);
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
在这个示例中,我们首先打开一个文件并获取文件的大小。然后使用 mmap
函数将文件映射到内存中,并获取映射的内存起始地址。我们将这个地址转换为字符型指针,然后就可以像操作普通的内存一样访问文件的数据了。
当你完成对映射数据的访问后,记得使用 munmap
函数来释放映射的内存。这样可以确保在程序退出时释放相关资源,避免内存泄漏。
注意
写函数声明和不写函数声明的区别
如果我们不写函数声明,那么在编译的时候大概率会优化这个函数,如果会把这个函数的内容直接拿出来放在函数调用处从而在so库中是不会存在这个函数名字的,以节省so文件空间和运行时解析的时间,如果我们在头文件中声明这个函数,那么就相当于强制使这个函数在so库文件中存在声明,因为我们可以单独拿出来这个头文件在其他so库文件中进行调用,如果优化了的话,这个函数在链接过程就会出错,所以linux不会优化这个函数。这里的优化是指会在so库文件中存在声明,在函数调用处也可能会把函数的内容直接移过来优化过程(这样就少了一次函数跳转)。
如果不把函数声明写在.h头文件中,而是写在cpp文件中:void getop();
作用是和写在.h文件一样的,同样在so库文件中是存在这个函数的,我们在其他so库中也可以调用到这个函数。
转载自:https://juejin.cn/post/7290801256490844217